Java泛型的拓展
Java泛型拓展
基础知识之协变与逆变
1 | // public final class Integer extends Number |
为什么Number的对象可以由Integer实例化,而ArrayList<Number>的对象却不能由ArrayList<Integer>实例化?List中的<? extends Number>声明其元素是Number或Number的派生类,为什么不能add Integer?为了解决这些问题,需要了解Java中的逆变和协变以及泛型中通配符用法。
Java中String类型是继承自Object的,姑且记做String ≦ Object,表示String是Object的子类型,String的对象可以赋给Object的对象。而Object的数组类型Object[],理解成是由Object构造出来的一种新的类型,可以认为是一种构造类型,记f(Object),那么可以这么来描述协变和逆变:
当A ≦ B时,如果有f(A) ≦ f(B),那么f叫做协变;
当A ≦ B时,如果有f(B) ≦ f(A),那么f叫做逆变;
如果上面两种关系都不成立则叫做不可变。
JAVA中泛型是不变的,可有时需要实现逆变与协变,怎么办呢?这时就需要通配符?。
协变
<? extends Number>实现了Java中的协变
1 | List<? extends Number> list = new ArrayList<>(); · |
<? extends Number>表示通配符?的上界为Number,即“? extends Number”可以代表Number或其子类,但代表不了Number的父类(如Object),因为通配符的上界是Number。
于是有? extends Number ≦ Number,则List<? extends Number> ≦ List< Number >。那么就有:
1 | List<? extends Number> list001 = new ArrayList<Integer>(); |
但是这里不能向list001、list002添加除null以外的任意对象。可以这样理解一下,List<Integer>可以添加Interger及其子类,List<Float>可以添加Float及其子类,List<Integer>、List<Float>都是List<? extends Animal>的子类型,如果能将Float的子类添加到List<? extends Animal>中,就说明Float的子类也是可以添加到List<Integer>中的,显然是不可行。故java为了保护其类型一致,禁止向List<? extends Number>添加任意对象,不过却可以添加null。
逆变
<? super>实现了泛型的逆变,比如:
1 | List<? super Number> list = new ArrayList<>(); |
? super Number 则表示通配符?的下界为Number。为了保护类型的一致性,因为? super Number可以是Object或其他Number的父类,因无法确定其类型,也就不能往List<? super Number >添加Number的任意父类对象。但是可以向List<? super Number>添加Number及其子类。
1 | List<? super Number> list001 = new ArrayList<Number>(); |
PECS
现在问题来了:究竟什么时候用extends什么时候用super呢?《Effective Java》给出了答案:
PECS: producer-extends, consumer-super.
PECS指“Producer Extends,Consumer Super”。换句话说,如果参数化类型表示一个生产者,就使用<? extends T>;如果它表示一个消费者,就使用<? super T>。
比如一个Stack API
1 | public class Stack<E>{ |
要实现pushAll(Iterable<E> src)方法,将src的元素逐一入栈:
1 | public void pushAll(Iterable<E> src){ |
假设有一个实例化Stack<Number>的对象stack,src有Iterable<Integer>与 Iterable<Float>;在调用pushAll方法时会发生type mismatch错误,因为Java中泛型是不可变的,Iterable<Integer>与 Iterable<Float>都不是Iterable<Number>的子类型。因此,应改为
1 | // Wildcard type for parameter that serves as an E producer |
要实现popAll(Collection<E> dst)方法,将Stack中的元素依次取出add到dst中,如果不用通配符实现:
1 | // popAll method without wildcard type - deficient! |
同样地,假设有一个实例化Stack<Number>的对象stack,dst为Collection<Object>;调用popAll方法是会发生type mismatch错误,因为Collection<Object>不是Collection<Number>的子类型。因而,应改为:
1 | public void popAll(Collection<? super E> dst){ |
在上述例子中,在调用pushAll方法时生产了E 实例(produces E instances),在调用popAll方法时dst消费了E 实例(consumes E instances)。Naftalin与Wadler将PECS称为Get and Put Principle。
java.util.Collections的copy方法(JDK1.7)完美地诠释了PECS:
1 | public static <T> void copy(List<? super T> dest, List<? extends T> src) { |
通配符的副作用
1 | //以盘子为例 |
//其实锅都在编译器中:
原因是编译器只知道容器内是Fruit或者它的派生类,但具体是什么类型不知道。可能是Fruit?可能是Apple?也可能是Banana,RedApple,GreenApple?编译器在看到后面用Plate赋值以后,盘子里没有被标上有“苹果”。而是标上一个占位符:CAP#1,来表示捕获一个Fruit或Fruit的子类,具体是什么类不知道,代号CAP#1。然后无论是想往里插入Apple或者Meat或者Fruit编译器都不知道能不能和这个CAP#1匹配,所以就都不允许。
所以通配符<?>和类型参数的区别就在于,对编译器来说所有的T都代表同一种类型。比如下面这个泛型方法里,三个T都指代同一个类型,要么都是String,要么都是Integer。
1 | public <T> List<T> fill(T... t); |
但通配符<?>没有这种约束,Plate<?>单纯的就表示:盘子里放了一个东西,是什么我不知道。
所以题主问题里的错误就在这里,Plate<? extends Fruit>里什么都放不进去。
下界<? super T>不影响往里存,但往外取只能放在Object对象里
使用下界<? super Fruit>会使从盘子里取东西的get( )方法部分失效,只能存放到Object对象里。set( )方法正常.
1 | p=new Plate(new Fruit()); |
因为下界规定了元素的最小粒度的下限,实际上是放松了容器元素的类型控制。既然元素是Fruit的基类,那往里存粒度比Fruit小的都可以。但往外读取元素就费劲了,只有所有类的基类Object对象才能装下。但这样的话,元素的类型信息就全部丢失。
总结 PECS原则:
最后看一下什么是PECS(Producer Extends Consumer Super)原则,已经很好理解了:
- 频繁往外读取内容的,适合用上界Extends。
- 经常往里插入的,适合用下界Super。
The principles behind this in Computer Science is named after
- Covariance - ? extends MyClass,
- Contravariance - ? super MyClass and
- Invariance/non-Variance - MyClass
The picture below should explain the concept.

(图片来源 https://stackoverflow.com/users/2707792/andrey-tyukin)
TODO :好好理解一下PECS 原则?到底什么时候是生产,什么时候是消费。生产了什么与消费了什么?