什么是泛型

Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
例如我们平常声明一个集合变量List<String> list = new ArrayList<String>(); 这里就是泛型的一种。

泛型的本质

泛型的本质是参数化类型,即给类型指定一个参数,然后在使用时再指定此参数具体的值,那样这个类型就可以在使用时决定了。这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

泛型的作用

提高了代码的复用性

  1. // 没有泛型之前
  2. private static int add(int a, int b) {
  3. System.out.println(a + "+" + b + "=" + (a + b));
  4. return a + b;
  5. }
  6. private static float add(float a, float b) {
  7. System.out.println(a + "+" + b + "=" + (a + b));
  8. return a + b;
  9. }
  10. private static double add(double a, double b) {
  11. System.out.println(a + "+" + b + "=" + (a + b));
  12. return a + b;
  13. }
  14. // 有了泛型之后
  15. private static <T extends Number> double add(T a, T b) {
  16. System.out.println(a + "+" + b + "=" + (a.doubleValue() + b.doubleValue()));
  17. return a.doubleValue() + b.doubleValue();
  18. }

保证了类的安全性

相当于告诉编译器每个集合接收的对象类型是什么,编译器在编译期就会做类型检查,告知是否插入了错误类型的对象,使得程序更加安全,增强了程序的健壮性。

  1. // 没有泛型的时候
  2. public static void noGeneric() {
  3. ArrayList names = new ArrayList();
  4. names.add("mikechen的互联网架构");
  5. names.add(123); //编译正常
  6. }
  7. // 有泛型的时候
  8. public static void useGeneric() {
  9. ArrayList<String> names = new ArrayList<>();
  10. names.add("mikechen的互联网架构");
  11. //names.add(123); //编译不通过
  12. }

消除强制转换

泛型的一个附带好处是,消除源代码中的许多强制类型转换,这使得代码更加可读,并且减少了出错机会。

  1. // 没有泛型的代码段需要强制转换
  2. List list = new ArrayList();
  3. list.add("hello");
  4. String s = (String) list.get(0);
  5. //当重写为使用泛型时,代码不需要强制转换
  6. List<String> list2 = new ArrayList<String>();
  7. list2.add("hello");
  8. String s2 = list2.get(0); // no cast

避免了不必要的装箱、拆箱操作,提高程序的性能

在非泛型编程中,将筒单类型作为Object传递时会引起Boxing(装箱)和Unboxing(拆箱)操作,这两个过程都是具有很大开销的。引入泛型后,就不必进行Boxing和Unboxing操作了,所以运行效率相对较高,特别在对集合操作非常频繁的系统中,这个特点带来的性能提升更加明显。

泛型变量固定了类型,使用的时候就已经知道是值类型还是引用类型,避免了不必要的装箱、拆箱操作。

  1. public static void main(String[] args) {
  2. // 没有泛型之前
  3. Object a = 1;//由于是object类型,会自动进行装箱操作。
  4. int b = (int)a;//强制转换,拆箱操作。这样一去一来,当次数多了以后会影响程序的运行效率
  5. // 有了泛型之后
  6. int value = getValue(1);
  7. }
  8. public static <T>T getValue(T a){
  9. return a;
  10. }

怎么使用泛型

泛型类

泛型类就是把泛型定义在类上

  1. public class 类名 <泛型类型1,...> {
  2. }

:::info 注意事项:泛型类型必须是引用类型(非基本数据类型) ::: 当然,这个后面的参数类型也是有规范的,不能像上面一样随意,通常类型参数我们都使用大写的单个字母表示:

  • T:任意类型的type
  • E:集合中元素的类型Element
  • K:k-v形式的Key
  • V:k-v形式的V

    1. public class Test6<T> {
    2. private T value;
    3. public Test6(T value) {
    4. this.value = value;
    5. }
    6. public T getValue() {
    7. return value;
    8. }
    9. public void setValue(T value) {
    10. this.value = value;
    11. }
    12. }
    1. public class Test7<E> {
    2. private List<E> data;
    3. public Test7(List<E> data) {
    4. this.data = data;
    5. }
    6. public E get(int index){
    7. return data.get(index);
    8. }
    9. }
    1. public class Test8<K,V> {
    2. private Map<K,V> data;
    3. public void setData(Map<K, V> data) {
    4. this.data = data;
    5. }
    6. public V get(K k){
    7. return data.get(k);
    8. }
    9. public static void main(String[] args) {
    10. Test8<String,Integer> data = new Test8<>();
    11. Map<String,Integer> map = new HashMap<>();
    12. map.put("a",97);
    13. data.setData(map);
    14. Integer a = data.get("a");
    15. System.out.println(a);
    16. }
    17. }

    泛型接口

    ```java public <泛型类型> 返回类型 方法名(泛型类型 变量名) {

}

  1. :::info
  2. 方法声明中定义的形参只能在该方法里使用,而接口、类声明中定义的类型形参则可以在整个接口、类中使用。当调用fun()方法时,根据传入的实际对象,编译器就会判断出类型形参T所代表的实际类型。
  3. :::
  4. ```java
  5. public interface Test9<T> {
  6. T show(T param);
  7. }
  8. class Test94String implements Test9<String>{
  9. @Override
  10. public String show(String param) {
  11. return param;
  12. }
  13. }
  14. class Test94Integer implements Test9<Integer>{
  15. @Override
  16. public Integer show(Integer param) {
  17. return param;
  18. }
  19. }
  20. class Test9main{
  21. public static void main(String[] args) {
  22. //Test9<String> test9 = new Test94Integer(); // 编译异常
  23. Test9<Integer> test9 = new Test94Integer(); // 编译正常
  24. // 或者干脆不指定类型,那么 new 什么类型都是可以的:
  25. Test9 test1= new Test94String();
  26. Test9 test2= new Test94String();
  27. }
  28. }

泛型方法

  1. 修饰符 <代表泛型的变量> 返回值类型 方法名(参数){
  2. }
  1. public class Test10 {
  2. public <T> T get(T t){
  3. System.out.println(t.getClass());
  4. System.out.println(t);
  5. return t;
  6. }
  7. public static void main(String[] args) {
  8. Test10 test10 = new Test10();
  9. Integer integer = test10.get(1);
  10. String a = test10.get("a");
  11. }
  12. }

这里可以看出,泛型方法随着我们的传入参数类型不同,他得到的类型也不同。泛型方法能使方法独立于类而产生变化。

泛型通配符

Java泛型的通配符是用于解决泛型之间引用传递问题的特殊语法, 主要有以下三类:

  • <?> :无边界的通配符,表示类型参数可以是任何类型
  • <? extends E> :固定上边界的通配符,表示类型参数必须是A或者是A的子类
  • <? extends E> :固定上边界的通配符,表示类型参数必须是A或者是A的超类型

    <?> :无边界的通配符

    无边界的通配符的主要作用就是让泛型能够接受未知类型的数据.

    <? extends E> :固定上边界的通配符

    使用固定上边界的通配符的泛型, 就能够接受指定类及其子类类型的数据。
    要声明使用该类通配符, 采用<? extends E>的形式, 这里的E就是该泛型的上边界。
    注意: 这里虽然用的是extends关键字, 却不仅限于继承了父类E的子类, 也可以代指显现了接口E的类

    <? extends E> :固定上边界的通配符

    使用固定下边界的通配符的泛型, 就能够接受指定类及其父类类型的数据.。
    要声明使用该类通配符, 采用<? super E>的形式, 这里的E就是该泛型的下边界.。
    注意: 你可以为一个泛型指定上边界或下边界, 但是不能同时指定上下边界。

    泛型的实现原理

    泛型本质是将数据类型参数化,它通过类型擦除的方式来实现,即编译器会在编译期间「擦除」泛型语法并相应的做出一些类型转换动作。
    例如:
    1. public class Caculate<T> {
    2. private T num;
    3. }
    我们定义了一个泛型类,定义了一个属性成员,该成员的类型是一个泛型类型,这个 T 具体是什么类型,我们也不知道,它只是用于限定类型的。
    反编译一下这个 Caculate 类:
    1. public class Caculate{
    2. public Caculate(){}
    3. private Object num;
    4. }
    发现编译器擦除 Caculate 类后面的两个尖括号,并且将 num 的类型定义为 Object 类型。

那么是不是所有的泛型类型都以 Object 进行擦除呢?大部分情况下,泛型类型都会以 Object 进行替换,而有一种情况则不是。那就是使用到了extends和super语法的有界类型,如:

  1. public class Caculate<T extends String> {
  2. private T num;
  3. }

这种情况的泛型类型,num 会被替换为 String 而不再是 Object。

这是一个类型限定的语法,它限定 T 是 String 或者 String 的子类,也就是你构建 Caculate 实例的时候只能限定 T 为 String 或者 String 的子类,所以无论你限定 T 为什么类型,String 都是父类,不会出现类型不匹配的问题,于是可以使用 String 进行类型擦除。

实际上编译器会正常的将使用泛型的地方编译并进行类型擦除,然后返回实例。但是除此之外的是,如果构建泛型实例时使用了泛型语法,那么编译器将标记该实例并关注该实例后续所有方法的调用,每次调用前都进行安全检查,非指定类型的方法都不能调用成功。

实际上编译器不仅关注一个泛型方法的调用,它还会为某些返回值为限定的泛型类型的方法进行强制类型转换,由于类型擦除,返回值为泛型类型的方法都会擦除成 Object 类型,当这些方法被调用后,编译器会额外插入一行 checkcast 指令用于强制类型转换,这一个过程就叫做『泛型翻译』。