Java泛型的拓展
Java泛型拓展
基础知识之协变与逆变
// public final class Integer extends Number
Number num = new Integer(1);
List<Number> list = new ArrayList<>();
list.add(new Integer(3));
ArrayList<Number> list = new ArrayList<Integer>(); //type mismatch 这两个根本就不是一个类型
List<? extends Number> list = new ArrayList<Number>();
list.add(new Integer(1)); //error
为什么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中的协变
List<? extends Number> list = new ArrayList<>(); ·
<? extends Number>
表示通配符?
的上界为Number
,即“? extends Number
”可以代表Number
或其子类,但代表不了Number
的父类(如Object
),因为通配符的上界是Number
。
于是有? extends Number ≦ Number
,则List<? extends Number> ≦ List< Number >
。那么就有:
List<? extends Number> list001 = new ArrayList<Integer>();
List<? extends Number> list002 = new ArrayList<Float>();
但是这里不能向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>
实现了泛型的逆变,比如:
List<? super Number> list = new ArrayList<>();
? super Number
则表示通配符?
的下界为Number
。为了保护类型的一致性,因为? super Number
可以是Object
或其他Number
的父类,因无法确定其类型,也就不能往List<? super Number >
添加Number
的任意父类对象。但是可以向List<? super Number>添加Number
及其子类。
List<? super Number> list001 = new ArrayList<Number>();
List<? super Number> list002 = new ArrayList<Object>();
list001.add(new Integer(3));
list002.add(new Integer(3));
PECS
现在问题来了:究竟什么时候用extends什么时候用super呢?《Effective Java》给出了答案:
PECS: producer-extends, consumer-super.
PECS指“Producer Extends,Consumer Super”。换句话说,如果参数化类型表示一个生产者,就使用<? extends T>
;如果它表示一个消费者,就使用<? super T>
。
比如一个Stack API
public class Stack<E>{
public Stack();
public void push(E e):
public E pop();
public boolean isEmpty();
}
要实现pushAll(Iterable<E> src)
方法,将src的元素逐一入栈:
public void pushAll(Iterable<E> src){
for(E e : src)
push(e)
}
假设有一个实例化Stack<Number>
的对象stack
,src
有Iterable<Integer>
与 Iterable<Float>
;在调用pushAll
方法时会发生type mismatch
错误,因为Java中泛型是不可变的,Iterable<Integer>
与 Iterable<Float>
都不是Iterable<Number>
的子类型。因此,应改为
// Wildcard type for parameter that serves as an E producer
public void pushAll(Iterable<? extends E> src){
for(E e : src)
push(e)
}
要实现popAll(Collection<E> dst)
方法,将Stack
中的元素依次取出add
到dst
中,如果不用通配符实现:
// popAll method without wildcard type - deficient!
public void popAll(Collection<E> dst) {
while (!isEmpty())
dst.add(pop());
}
同样地,假设有一个实例化Stack<Number>
的对象stack
,dst
为Collection<Object>
;调用popAll方法是会发生type mismatch
错误,因为Collection<Object>
不是Collection<Number>
的子类型。因而,应改为:
public void popAll(Collection<? super E> dst){
while(!isEmpty()){
dst.add(pop);
}
}
在上述例子中,在调用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:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");
if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i<srcSize; i++)
dest.set(i, src.get(i));
} else {
ListIterator<? super T> di=dest.listIterator();
ListIterator<? extends T> si=src.listIterator();
for (int i=0; i<srcSize; i++) {
di.next();
di.set(si.next());
}
}
}
通配符的副作用
//以盘子为例
class plate<T>{
private T item;
public Plate(T t){
item = t;
}
public void set(T t){
item = t;
}
public void get(){
return item;
}
}
class Fruit{}
class Apple extends Apple{}
//上界<? enteds T>只能取,不能存。
//code block
Plate<? extends Fruit> p = new plate<Apple>(new Apple());
//不能存储任何元素;
p.set(new Fruit());
p.set(new Apple());
//读取出来的东西只能放在Fruit或者是它的基类里。
Fruit f = p.get();
Object f1 = p.get();
Apple af = p.get() // ERROR;
//其实锅都在编译器中:
原因是编译器只知道容器内是Fruit或者它的派生类,但具体是什么类型不知道。可能是Fruit?可能是Apple?也可能是Banana,RedApple,GreenApple?编译器在看到后面用Plate赋值以后,盘子里没有被标上有“苹果”。而是标上一个占位符:CAP#1,来表示捕获一个Fruit或Fruit的子类,具体是什么类不知道,代号CAP#1。然后无论是想往里插入Apple或者Meat或者Fruit编译器都不知道能不能和这个CAP#1匹配,所以就都不允许。
所以通配符<?>
和类型参数的区别就在于,对编译器来说所有的T
都代表同一种类型。比如下面这个泛型方法里,三个T都指代同一个类型,要么都是String,要么都是Integer。
public <T> List<T> fill(T... t);
但通配符<?>
没有这种约束,Plate<?>
单纯的就表示:盘子里放了一个东西,是什么我不知道。
所以题主问题里的错误就在这里,Plate<? extends Fruit>
里什么都放不进去。
下界<? super T>
不影响往里存,但往外取只能放在Object对象里
使用下界<? super Fruit>会使从盘子里取东西的get( )方法部分失效,只能存放到Object对象里。set( )方法正常.
p=new Plate(new Fruit());
//存入元素正常
p.set(new Fruit());
p.set(new Apple());
//读取出来的东西只![KjDLw](../../../Desktop/KjDLw.png)能存放在Object类里。
Apple newFruit3=p.get(); //Error
Fruit newFruit1=p.get(); //Error
Object newFruit2=p.get();
因为下界规定了元素的最小粒度的下限,实际上是放松了容器元素的类型控制。既然元素是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 原则?到底什么时候是生产,什么时候是消费。生产了什么与消费了什么?