# 泛型(Generic Type)

  • 允许在定义接口、类、方法时使用类型形参,类型形参在整个接口、类体、方法内可当成类型使用,这个类型形参将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型参数,类型实参
  • 传入实际的类型参数不能是基本数据类型
  • 同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的
  • 在编译期间,所有的泛型信息都会被擦掉:对于泛型类,编译器会将泛型代码转换为普通的非泛型代码(将类型参数 T 擦除,替换为 Object,插入必要的强制类型转换),因此在程序运行时并不知道泛型的实际类型参数;但某些声明的泛型信息会保留在 class 文件的 Constant pool 中的 Signature
  • 常见的类型形参:T、E、K、V 等
  • 泛型的意义和作用
    1. 类型的参数化,可以把类型像方法的参数那样传递
    2. 泛型使编译器可以在编译期间对类型进行检查以提高类型安全,减少运行时由于对象类型不匹配引发的异常

# 定义泛型接口、类

  • 在接口、类后增加尖括号,尖括号里放一个数据类型,代表类型形参
  • Java 7 泛型的“菱形”语法:创建对象时,在构造器后不需要带完整的泛型信息,只要给出一对尖括号(<>)即可
  • 在创建带泛型声明的自定义类中定义构造器时,不要增加泛型声明
  • 定义实现类、子类时,使用带泛型声明的接口、父类时不能再包含类型形参,
  • 如果使用带泛型声明的类时没有传入实际的类型参数,系统会把类 <T> 里的 T 形参当成 Object 类型处理
  • 不管泛型的实际类型参数是什么,它们在运行时总有同样的类(class)
  • 在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用类上定义的类型形参;instanceof 运算符后不能使用泛型类
  • 泛型不存在继承的关系,如 Collection<String> 不是 Collection<Object > 的子类型
// 定义 Box 类时使用了泛型声明
public class Box<T> {
    // 使用 T 类型形参定义实例变量
    private T info;
    // 定义构造器时,不要增加泛型声明
    public Box() {
    }
    
    // 下面方法中使用 T 类型形参来定义构造器
    public Box(T info) {
        this.info = info;
    }
    
    public T getInfo() {
        return this.info;
    }
}

// 使用 Box 类时为 T 形参传入 String 类型
public class B1 extends Box<String> {
    // 重写父类的方法、返回值
    // 与父类 Box<String> 的返回值完全相同
    public String getInfo() {
        return "子类" + super.getInfo();
    }
}

public class B2 extends Box {
    // 重写父类的方法
    public String getInfo() {
        // super.getInfo() 方法返回值是 Object 类型,
        // 所以加 toString() 才返回 String 类型
        return super.getInfo().toString();
    }
}

public class B3<T> extends Box<T> {
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

# 泛型方法

  • 在声明方法时定义一个或多个类型形参,放在方法修饰符和方法返回值类型之间
  • 方法中的泛型参数无须显式传入实际类型参数,编译器会根据实参推断类型形参的值
修饰符 <T, S> 返回值类型 方法名(形参列表) {
    //方法体
}
1
2
3

# 类型通配符

  • 使用类型通配符,如: List<?> (表示元素类型未知的 List)、Set<?>、Collection<?>、Map<?, ?>

  • 设定类型通配符的上限

    // 表示所有以 Shape 为父类的类型,元素的类型是继承自 Shape
    public void drawAll(List<? extends Shape> shapes) {}
    
    1
    2
  • 设定类型形参的上限(至多有一个父类上限,可以有多个接口上限)

    // 表明 T 类型必须是 Number 类或其子类,并必须实现 java.io.Serializable 接口
    public class Box<T extends Number & java.io.Serializable> {
    }
    
    1
    2
    3
  • 设定类型通配符的下限

    // TreeSet<E> 中的一个构造器
    // 表示所有以 E 为子类的类型,元素的类型是 E 的父类
    TreeSet(Comparator<? super E> c)
    
    1
    2
    3
  • PECS(Producer Extends Consumer Super)原则:只从集合中读取数据,即集合是生产者,应设定上限 extends;只往集合中增加数据,即集合是消费者,应设定下限 super

# 擦除和转换

  • 擦除:当把一个带泛型的对象赋给一个不带泛型的变量时,所有在尖括号之间的类型信息都将被扔掉
  • 堆污染:对于形参个数可变的方法,该形参的类型又是泛型时,容易导致“堆污染”
    @SafeVarargs 抑制堆污染警告的注解
List<Integer> li = new ArrayList<>();
li.add(123);
List list = li; // 泛型的擦除
// 下面代码引起“未经检查的转换”警告,编译、运行时完全正常
List<String> ls = list; // 泛型的转换
// 当试图把 ls 里的元素当成 String 类型的对象取出时,将引发 ClassCastException 异常
// System.out.println(ls.get(0));

// Arrays 类中的 asList 方法
public static <T> List<T> asList(T... a)
1
2
3
4
5
6
7
8
9
10

泛型因为类型擦除会导致泛型方法 T 占位符被替换为 Object,子类如果使用具体类型覆盖父类实现,编译器会生成桥接方法。这样既满足子类方法重写父类方法的定义,又满足子类实现的方法有具体的类型。

# 泛型与数组

  • 数组元素的类型不能包含类型变量或类型形参,除非是无上限的类型通配符
Updated at: 2023-01-07 17:49:19