逆变Contravariance与协变Covariance
先用一张图解释下
Java数组是协变的
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}; |
在编译时,上面的代码不会报错,但是运行时第4行代码会抛出ArrayStoreException
。很明显,即使通过一个Number[]
引用,你也不可能把一个浮点数放入一个事实上的Integer[]
数组。可以骗过编译器的代码,在运行时类型系统下并不能正确通过。我们甚至可以向协变数组中加入元素,因为Java的运行时系统知道这个数组的真实类型,所以第3行代码在添加元素时,是可以正常执行的。
Java泛型中的类型擦除
因为JDK1.5中才引入泛型机制,为了兼容旧的字节码,Java规范在编译时对泛型进行了类型擦除。也就是说我们使用的所有泛型仅仅存在于编译期间,当通过编译器检查后,泛型信息都会被删除。在运行时,JVM处理的都是没有携带泛型信息的类型。因此我们有下面的问题:
1 | List<Integer> IntList = new ArrayList<Integer>(); |
如果第4行,编译器没有报错。顺其自然,第5行也会通过编译。那么到了运行时,由于泛型信息被删除了,第5行的代码也会被正常执行,一个本应该是储存Integer
的List
,却被添加了一个浮点数。为了避免类型系统被破坏,编译器必须阻止我们进行第4行这样的操作。在Java泛型中,List<Integer>
不是List<Number>
的子类。
泛型擦除的机制消弱了多态的使用。如果我们有下面这个函数:
1 | static long sum(Number[] numbers) { |
我们可以很自然的以下面的方式使用这个函数:
1 | Integer[] IntArray = {1,2,3,4,5}; |
因为数组是协变的,所以把子类型实参传入父类型形参是完全没问题的。多态很正确的被运用了。
但是,如果我们的函数使用了泛型:
1 | static long sum(List<Number> numbers) { |
因为泛型擦除可能导致的List
被污染的原因,我们不能把List<Integer>
当成List<Number>
的子类型,所以把Java的泛型称作是不可变的 (invariant)。
很显然,这是一个问题,我们需要通过某种机制,让泛型也可以用在多态中。
泛型中的协变 Covariance
使用通配符? extends T
,其中T
是一个基类型,或者说父类。然后我们就可以实现如下代码:
1 | List<? extends Number> NumList = new ArrayList<Integer>(); |
并且,我们可以从NumList
中读取元素:
1 | Number n = NumList.get(0); |
因为我们可以确定,不管从NumList
中拿到什么元素,都一定是Number
的子类(泛型通配符? extends Number
所规定的),所以父类Number
的引用一定可以指向这个子类元素。
上面的出现的函数可以改写为:
1 | static long sum(List<? extends Number> numbers) {...} |
但是,我们不能向一个协变泛型的结构中加入任何元素。
1 | NumList.add(45L); //compiler error |
因为运行时的泛型擦除,而编译时的信息不足够确定NumList
的真实类型,编译器并不知道添加什么类型的元素才是合法的。
所以如果一个容器是协变的,就只能设置为只读。否则我们能把一些特殊的容器协变到更一般的容器后,就可以再往里面添加进不应该储存的类型。协变结构可读,不可写。
逆变 Contravariance
使用通配符? super T
,其中T
是一个基类型,或者说父类,我们可以向逆变结构中添加任何T
的子类。逆变结构可写,不可读。
1 | List<? super Number> NumList = new ArrayList<Object>(); |
一个储存Object
的List
,自然可以放入Number
类型的元素。逻辑上的List<Object>
应该是List<Number>
的父类,但父类被反过来赋值给子类的引用,所以称作逆变。
但是由于规定了 ? super Number
的限制,这表明我们需要在容器中储存Number
及其子类型,在第4行我们不能再向这个List
中添加Object
类型的元素。
从逆变结构中读取元素是不被允许的。在第5行,我们想要得到一个Number
,但是这不会被编译器允许,因为不能保证这个List
中均是Number
类型的元素。另外说明一下,Object n = NumList.get(0)
是允许的,但是这和原本的泛型类型<Object>
没有任何关系,因为Object
在Java中是所有类的父类,所以Java允许我们把任何类型当作Object
对象,造成了这种可以从逆变容器中取出元素的特殊情况。
PECS原则
Producer Extends Consumer Super Principle
PECE原则是从一个容器角度来描述的
- 一个泛型结构的用途是让别人来读取内容(生产者协变),需要使用
? extends T
。 - 一个泛型结构的用途是供别人写入内容(消费者逆变),需要使用
? super T
。
1 | public static void copy(List<? extends Number> source, List<? super Number> destination) { |
1 | List<Integer> IntList = Arrays.asList(1,2,3,4); //Producer |
总结
在泛型中使用逆变和协变,可以很好的克服Java中泛型擦除带来的负面影响,让泛型和多态可以快乐地一起玩耍。
当我们已经知道类型之间的继承关系,比如Number
类型是Integer
类型的父类。我们就可以有以下结论:
- 协变(covariance):
List<Number>
是List<Integer>
的父类,它们维持内部参数的关系不变。 - 逆变(contravariance):
List<Number>
是List<Integer>
的子类,它们的关系被反转了。 - 不变(invariance):两者没有任何子类型关系,不能互相替代。