在介绍泛型的通配符之前,首先需要了解下协变与逆变的相关知识。
定义
逆变与协变用来描述类型转换(type transformation)后的继承关系,其定义:如果 𝐴、𝐵 表示类型,𝑓(⋅) 表示类型转换,≤ 表示继承关系(比如 𝐴≤𝐵 表示 𝐴 是由 𝐵 派生出来的子类)
- 𝑓(⋅) 是逆变(contravariant)的,当 𝐴≤𝐵 时有 𝑓(𝐵)≤𝑓(𝐴) 成立;
- 𝑓(⋅) 是协变(covariant)的,当 𝐴≤𝐵 时有 𝑓(𝐴)≤𝑓(𝐵) 成立;
- 𝑓(⋅) 是不变(invariant)的,当 𝐴≤𝐵 时上述两个式子均不成立,即 𝑓(𝐴) 与 𝑓(𝐵) 相互之间没有继承关系。
数组是协变的
1 | Number[] numbers = new Number[3]; |
包装类 Integer、Double、Long 是 Number 的子类,numbers 数组中的元素的类型可以是任何 Number 的子类。我们称 Java 数组是协变的 (Covariant)。
不仅如此,下面的代码也是合法的:
1 | Integer[] IntArray = {1,2,3,4}; |
根据协变的定义,因为数组是协变的,所以 Integer[] 是 Number[] 的子类。可以将子类型的数组赋予基类型的数组引用。即父类的引用可以指向子类对象。
但是这会导致一个有趣的问题:
1 | Integer[] IntArray = {1,2,3,4}; |
在编译时,上面的代码不会报错,但是运行时最后一行代码会抛出 ArrayStoreException。很明显,即使通过一个 Number[] 引用,也不能将一个浮点数放入一个事实上的 Integer[] 数组。因为在运行时知道这个数组的真实类型为存放 Integer 类型的数组。
泛型是不变的
要理解泛型是不变的,需要在此之前说明泛型的擦除机制。
因为 JDK1.5 中才引入泛型机制,为了兼容旧的字节码,Java 规范在编译时对泛型进行了类型擦除。也就是说我们使用的所有泛型仅仅存在于编译期间,当通过编译器检查后,泛型信息都会被删除。在运行时,JVM 处理的都是没有携带泛型信息的类型。
1 | public class ErasedTypeEquivalence { |
上面的执行结果为 true。
尽管 ArrayList<String> 和 ArrayList<Integer> 看上去是不同的类型,但在运行时实际上是相同的类型。这两种类型都被擦除成它们的原生类型,即 ArrayList。
再看下面一段代码:
1 | class Fruit {} |
与数组不同,泛型没有内建的协变类型。虽然 Apple 是 Fruit 的子类,但 ArrayList<Apple>
并不是 List<Fruit>
的子类,NonCovariantGenerics 直接在编译时报错了。
泛型虽然是不变的,但有时需要实现协变和逆变,这时就要用到泛型通配符了。