Java入门-协变与逆变

在介绍泛型的通配符之前,首先需要了解下协变与逆变的相关知识。

定义

逆变与协变用来描述类型转换(type transformation)后的继承关系,其定义:如果 𝐴、𝐵 表示类型,𝑓(⋅) 表示类型转换,≤ 表示继承关系(比如 𝐴≤𝐵 表示 𝐴 是由 𝐵 派生出来的子类)

  • 𝑓(⋅) 是逆变(contravariant)的,当 𝐴≤𝐵 时有 𝑓(𝐵)≤𝑓(𝐴) 成立;
  • 𝑓(⋅) 是协变(covariant)的,当 𝐴≤𝐵 时有 𝑓(𝐴)≤𝑓(𝐵) 成立;
  • 𝑓(⋅) 是不变(invariant)的,当 𝐴≤𝐵 时上述两个式子均不成立,即 𝑓(𝐴) 与 𝑓(𝐵) 相互之间没有继承关系。

数组是协变的

1
2
3
4
Number[] numbers = new Number[3];
numbers[0] = new Integer(10);
numbers[1] = new Double(3.14);
numbers[2] = new Long(99L);

包装类 Integer、Double、Long 是 Number 的子类,numbers 数组中的元素的类型可以是任何 Number 的子类。我们称 Java 数组是协变的 (Covariant)。

不仅如此,下面的代码也是合法的:

1
2
3
Integer[] IntArray = {1,2,3,4};
Number[] NumberArray = IntArray;
Number n = NumberArray[0]; //从一个协变结构中读取元素

根据协变的定义,因为数组是协变的,所以 Integer[] 是 Number[] 的子类。可以将子类型的数组赋予基类型的数组引用。即父类的引用可以指向子类对象。

但是这会导致一个有趣的问题:

1
2
3
4
Integer[] IntArray = {1,2,3,4};
Number[] NumberArray = IntArray;
NumberArray[0] = 9;
NumberArray[0] = 3.14; //尝试污染一个Integer数组 runtime error

在编译时,上面的代码不会报错,但是运行时最后一行代码会抛出 ArrayStoreException。很明显,即使通过一个 Number[] 引用,也不能将一个浮点数放入一个事实上的 Integer[] 数组。因为在运行时知道这个数组的真实类型为存放 Integer 类型的数组。

泛型是不变的

要理解泛型是不变的,需要在此之前说明泛型的擦除机制。

因为 JDK1.5 中才引入泛型机制,为了兼容旧的字节码,Java 规范在编译时对泛型进行了类型擦除。也就是说我们使用的所有泛型仅仅存在于编译期间,当通过编译器检查后,泛型信息都会被删除。在运行时,JVM 处理的都是没有携带泛型信息的类型。

1
2
3
4
5
6
7
public class ErasedTypeEquivalence {
public static void main(String[] args) {
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1 == c2);
}
}

上面的执行结果为 true。

尽管 ArrayList<String> 和 ArrayList<Integer> 看上去是不同的类型,但在运行时实际上是相同的类型。这两种类型都被擦除成它们的原生类型,即 ArrayList。

再看下面一段代码:

1
2
3
4
5
6
7
8
9
10
11
class Fruit {}
class Apple extends Fruit {}

public class NonCovariantGenerics {
List<Fruit> flist = new ArrayList<Apple>(); // 编译错误
}

public class CovariantArrays {
public static void main(String[] args) {
Fruit[] fruit = new Apple[10]; // 编译正确
}

与数组不同,泛型没有内建的协变类型。虽然 Apple 是 Fruit 的子类,但 ArrayList<Apple> 并不是 List<Fruit> 的子类,NonCovariantGenerics 直接在编译时报错了。

泛型虽然是不变的,但有时需要实现协变和逆变,这时就要用到泛型通配符了。