Java 泛型(generics)也是 JDK 5 中引入的一个新特性,泛型提供了编译时类型安全的检测机制,该机制允许开发者在编译时检测到非法的参数类型。泛型的本质是参数化类型,即所操作的数据类型被指定为一个参数。

在没有泛型的情况下,我们只能通过对类型 Object 的引用来实现参数的任意化,这带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。编译器对于强制类型转换错误的情况可能不提示错误,在运行的时候才出现异常,这是本身就是一个安全隐患。

那么泛型的好处就是在编译的时候能够检查类型安全,并且所有的强制转换都是自动和隐式的。

  1. public class GlmapperGeneric<T> {
  2. private T t;
  3. public void set(T t) { this.t = t; }
  4. public T get() { return t; }
  5. public void specifyType(){
  6. GlmapperGeneric<String> glmapperGeneric = new GlmapperGeneric();
  7. glmapperGeneric.set("test");
  8. // get时不需要强制类型转换
  9. String test = glmapperGeneric.get();
  10. }
  11. }

泛型擦除及局限

Java 语言的泛型实现方式是擦拭法(Type Erasure)。所谓擦拭法是指,虚拟机对泛型其实一无所知,所有的工作都是编译器做的。在编译过程中,类型变量会被擦除,并替换为第一个限定类型,对于无限定的变量则替换为 Object。例如,我们编写了一个泛型类 Pair,这是编译器看到的代码:

  1. public class Pair<T> {
  2. private T first;
  3. public T getFirst() {
  4. return first;
  5. }
  6. }

而虚拟机根本不知道泛型,这是虚拟机执行的代码:

  1. public class Pair {
  2. private Object first;
  3. public Object getFirst() {
  4. return first;
  5. }
  6. }

因此,Java 使用擦拭法实现泛型,导致了:

  • 编译器把类型 视为 Object
  • 编译器根据 实现安全的强制转型

所以,Java 的泛型是由编译器在编译时实行的,编译器内部永远把所有类型 T 视为 Object 处理,但是在需要转型的时候,编译器会根据 T 的类型自动为我们实行安全地强制转型。

Java 泛型的局限:

  • 不能是基本类型,例如 int,因为实际类型是 Object,而 Object 类型无法持有基本类型。

  • 无法取得带泛型的 Class。因为 T 是 Object,我们对 Pair 和 Pair 类型获取 Class 时获取到的都是同一个 Class,即所有泛型实例,无论 T 的类型是什么,getClass() 都返回同一个 Class 实例,因为编译后它们全部都是 Pair

    1. public static void main(String[] args) {
    2. Pair<String> p1 = new Pair<>("Hello");
    3. Pair<Integer> p2 = new Pair<>(123);
    4. System.out.println(p1.getClass() == p2.getClass()); // true
    5. System.out.println(p1.getClass() == Pair.class); // true
    6. }
  • 不能实例化 T 类型

  • 不能在静态字段或方法中引用类型变量 ```java public class Singleton {
  • // ERROR

    // private static T instance; // // private static T getInstance() { // return instance; // } }

    1. - 异常处理中,既不能抛出也不能捕获泛型类的对象
    2. <a name="ZQiWg"></a>
    3. # 泛型继承
    4. 一个类可以继承自一个泛型类。例如父类的类型是 Pair<Integer>,子类的类型是 IntPair,可以这么继承:
    5. ```java
    6. public class IntPair extends Pair<Integer> {
    7. }

    使用的时候,因为子类 IntPair 并没有泛型类型,所以正常使用即可:

    1. IntPair ip = new IntPair(1);

    前面讲了,我们无法获取 Pair 中的 T 类型,即给定一个变量 Pair p,无法从 p 中获取到 Integer 类型。但在父类是泛型类型的情况下,编译器就必须把类型 T 保存到子类的 class 文件中,不然编译器就不知道 IntPair 只能存取 Integer 这种类型。

    在继承了泛型类型的情况下,子类可以获取父类的泛型类型。例如:IntPair 可以获取到父类的泛型类型 Integer,获取父类的泛型类型代码比较复杂:

    1. public class Main {
    2. public static void main(String[] args) {
    3. Class<IntPair> clazz = IntPair.class;
    4. Type t = clazz.getGenericSuperclass();
    5. if (t instanceof ParameterizedType) {
    6. ParameterizedType pt = (ParameterizedType) t;
    7. Type[] types = pt.getActualTypeArguments(); // 可能有多个泛型类型
    8. Type firstType = types[0]; // 取第一个泛型类型
    9. Class<?> typeClass = (Class<?>) firstType;
    10. System.out.println(typeClass); // Integer
    11. }
    12. }
    13. }
    14. class Pair<T> {
    15. private T first;
    16. private T last;
    17. public Pair(T first, T last) {
    18. this.first = first;
    19. this.last = last;
    20. }
    21. public T getFirst() {
    22. return first;
    23. }
    24. public T getLast() {
    25. return last;
    26. }
    27. }
    28. class IntPair extends Pair<Integer> {
    29. public IntPair(Integer first, Integer last) {
    30. super(first, last);
    31. }
    32. }

    泛型通配符

    在定义泛型类,泛型方法,泛型接口的时候经常会碰见很多不同的通配符,比如 T,E,K,V 等。本质上这些都是通配符,没有什么区别,只不过是编码时的一种约定俗成的东西,并不会影响程序的正常运行。对于不确定或者不关心实际要操作的类型,可以使用无界通配符,即 <?>,表示可以持有任何类型。

    1. extends

    在类型参数中使用 <? extends E> 表示这个泛型中的参数必须是 E 或者 E 的子类

    1. List<? extends Number> numberArray = new ArrayList<Number>(); // Number 是 Number 类型的
    2. List<? extends Number> numberArray = new ArrayList<Integer>(); // Integer 是 Number 的子类
    3. List<? extends Number> numberArray = new ArrayList<Double>(); // Double 是 Number 的子类

    上面三个操作都是合法的, 因为 <? extends Number> 规定了泛型通配符的上界,即我们实际上的泛型必须要是 Number 类型或其子类, 而 Number、Integer、Double 都是 Number 的子类。

    根据上面的例子,对于 List<? extends Number> numberArray 对象:

    • 我们能够从 numberArray 中读取到 Number 对象,因为 numberArray 中包含的元素是 Number 类型或 Number 的子类型。但我们不能从 numberArray 中读取到 Integer 类型,因为 numberArray 中可能也保存的是 Double 类型。

    image.png

    • 我们不能添加 Number 到 numberArray 中,因为 numberArray 有可能是 List 类型,同理,也不能添加 Integer 和 Double。如下图,编译会报错!

    image.png

    我们不能添加任何对象到 List<? extends T> 中,因为我们不能确定一个 <? extends T> 对象实际的类型是什么,因此就不能确定插入的元素的类型是否和这个 List 匹配。List<? extends T> 唯一能保证的是我们从这个 list 中读取的元素一定是 T 类型的。

    即一句话总结:使用 extends 通配符表示可以读,不能写。

    此外,一个类型变量或通配符也可以有多个限定,限定类型之间用 & 分割,例如:

    1. <T extends Comparable & Serializable>

    在 Java 的继承中,可以根据需要拥有多个接口超类型,但最多有一个限定可以是类。如果有一个类作为限定,那么它必须是限定列表中的第一个限定。

    2. super

    和 extends 通配符相反,<? super T> 描述了通配符下界,即具体的泛型参数必须是 T 类型或它的父类。

    1. // 在这里, Integer 可以认为是 Integer 的 "父类"
    2. List<? super Integer> array = new ArrayList<Integer>();
    3. // Number 是 Integer 的 父类
    4. List<? super Integer> array = new ArrayList<Number>();
    5. // Object 是 Integer 的 父类
    6. List<? super Integer> array = new ArrayList<Object>();
    • 我们不能保证可以从 array 对象中读取到 Integer 类型的数据,因为 array 可能是 List 类型

    • 我们可以添加 Integer 对象到 array 中,也可以添加 Integer 的子类对象到 array 中,但我们不能添加 Double、Number、Object 等不是 Integer 的子类的对象到 array 中。

      3. extends 和 super 区别

      作为方法参数,<? extends T> 和 <? super T> 的区别在于:

    • <? extends T> 允许调用读方法 T get() 获取 T 的引用,但不允许调用写方法 set(T) 传入 T 的引用,传入 null 值除外。

    • <? super T> 允许调用写方法 set(T) 传入 T 的引用,但不允许调用读方法 T get() 获取 T 的引用,获取 Object 除外。

    一个是允许读不允许写,另一个是允许写不允许读。这里有个经典案例,我们来看 Java 标准库的 Collections 类定义的 copy() 方法:

    1. public class Collections {
    2. // 把src的每个元素复制到dest中:
    3. public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    4. for (int i = 0; i < src.size(); i++) {
    5. T t = src.get(i);
    6. dest.add(t);
    7. }
    8. }
    9. }

    它的作用是把一个 List 的每个元素依次添加到另一个 List 中。它的第一个参数 List<? super T> 表示的是目标 List,第二个参数 List<? extends T> 表示要复制的 List。我们可以简单地用 for 循环实现复制。在 for 循环中可以看到,对于类型 <? extends T> 的变量 src,我们可以安全地获取类型 T 的引用,而对于类型 <? super T> 的变量 dest,我们可以安全地传入 T 的引用。

    这个 copy() 方法的定义就完美地展示了 extends 和 super 的意图:

    • copy() 方法内部不会读取 dest,因为不能调用 dest.get() 来获取 T 的引用
    • copy() 方法内部也不会修改 src,因为不能调用 src.add(T)。

    这是由编译器检查来实现的。如果在方法代码中意外修改了 src 或者意外读取了 dest,就会导致一个编译错误。并且这个 copy() 方法的另一个好处是可以安全地把一个 List 添加到 List,但是无法反过来添加。而这些都是通过 super 和 extends 通配符,并由编译器强制检查来实现的。

    那何时该使用 extends 何时该使用 super 呢?为便于记忆,我们可以用 PECS 原则:Producer Extends Consumer Super

    • Producer Extends:如果我们需要一个 List 提供类型为 T 的数据(即希望从 List 中读取 T 类型的数据),那我们需要使用 <? extends T>但我们不能向这个 List 添加数据。

    • Consumer Super:如果我们需要一个 List 来消费 T 类型的数据(即希望将 T 类型的数据写入 List 中),那我们需要使用 <? super T>,但这个 List 不能保证从它读取的数据的类型。

    • 如果我们既希望读取,也希望写入,那我们就必须明确地声明泛型参数的类型,例如 List

    4. ?和 T 的区别

    实际上,Java 的泛型还允许使用无限定通配符(Unbounded Wildcard Type),即 <?>。

    :::info

    是一个确定的类型,通常用于泛型类和泛型方法的定义。<?> 是一个不确定的类型,不属于类型变量,因此不能在代码中使用 ? 作为一种类型,通常用于泛型方法的调用代码和形参,但不能用于定义类和泛型方法。多数情况下可以引入泛型参数 来消除 <?> 通配符。 :::

    通过 来确保泛型参数的一致性:

    1. // 通过<T>来确保泛型参数的一致性
    2. public <T extends Number> void test(List<T> dest, List<T> src)
    3. // 而通配符是不确定的,所以这个方法不能保证两个List具有相同的元素类型
    4. public void test(List<? extends Number> dest, List<? extends Number> src)

    类型参数可以多重限定而通配符不行:

    1. public class MultiLimit implements MultiLimitInterfaceA , MultiLimitInterfaceB {
    2. /**
    3. * 使用"&"符号设定多重边界(Multi Bounds)
    4. */
    5. public static<T extends MultiLimitInterfaceA & MultiLimitInterfaceB> void test(T t){
    6. }
    7. }
    8. // 接口A
    9. interface MultiLimitInterfaceA { }
    10. // 接口B
    11. interface MultiLimitInterfaceA { }

    使用 & 符号可以设定多重边界,这要求泛型类型 T 必须是 MultiLimitInterfaceA 和 MultiLimitInterfaceB 的共有子类型,此时变量 t 就具有了所有限定的方法和属性。对于通配符来说,因为它不是一个确定的类型,所以不能进行多重限定。

    通配符可以使用超类限定而类型参数不行:

    1. // 类型参数T只具有一种类型限定方式
    2. T extends A
    3. // 通配符?可以进行两种限定
    4. ? extends A
    5. ? super A