转载:摘抄自《Kotlin极简教程》-陈光剑
逆变与协变
Animal类型(简记为F, Father)是Dog类型(简记为C, Child)的父类型,我们把这种父子类型关系 简记为F <| C。
而List, List的类型,我们分别简记为f(F), f( C)。
那么我们可以这么来描述协变和逆变:
当F <| C 时, 如果有f(F) <| f( C),那么f叫做协变(Convariant); 当F <| C 时, 如果有f( C) <| f(F), 那么f叫做逆变(Contravariance)。 如果上面两种关系都不成立则叫做不可变。
协变和逆协变都是类型安全的。
Java中泛型是不变的,可有时需要实现逆变与协变,怎么办呢? 通配符上线
<? extends T> 实现了泛型的协变
List<? extends Number> list = new ArrayList<>();
这里的 ? extends Number 表示的是Number类或其子类,我们简记为C。 这里 C <| Number ,这个关系成立: List <| List< Number > 。既有
List<? extends Number> list1 = new ArrayList<Integer>();
List<? extends Number> list2 = new ArrayList<Float>();
但是这里不能向list1、list2添加除null以外的任意对象。
list1.add(null);
list2.add(null);
list1.add(new Integer(1)); // error
list2.add(new Float(1.1f)); // error
因为,List可以添加Interger及其子类,List可以添加Float及其子类,List、List都是 List<? extends Number> 的子类型,如果能将Float的子类添加到 中,那么也能将Integer 的子类添加到 List<? extends Number> 中, 那么这时候 里面将会持有各 种Number子类型的对象(Byte,Integer,Float,Double等等)。Java为了保护其类型一致,禁止 向List<? extends Number>添加任意对象,不过可以添加null。
<? super T> 实现了泛型的逆变
List<? super Number> list = new ArrayList<>();
? super Number 通配符则表示的类型下界为Number。即这里的父类型F是 ? super Number , 子类 型C是Number。即当F <| C , 有f( C) <| f(F) , 这就是逆变。代码示例:
List<? super Number> list3 = new ArrayList<Number>();
List<? super Number> list4 = new ArrayList<Object>();
list3.add(new Integer(3));
list4.add(new Integer(4));
也就是说,我们不能往 List<? super Number > 中添加Number的任意父类对象。但是可以向List<? super Number >添加Number及其子类对象。
PECS
现在问题来了:我们什么时候用extends什么时候用super呢?《Effective Java》给出了答案:
PECS: producer-extends, consumer-super
比如,一个简单的Stack API:
public class Stack<E>{
public Stack();
public void push(E e):
public E pop();
public boolean isEmpty();
}
要实现pushAll(Iterable src)方法,将src的元素逐一入栈:
public void pushAll(Iterable<E> src){
for(E e : src)
}
假设有一个实例化Stack的对象stack,src有Iterable与 Iterable;
在调用pushAll方法时会发生type mismatch错误,因为Java中泛型是不可变的,Iterable与 Iterable都 不是Iterable的子类型。
因此,应改为
// Wildcard type for parameter that serves as an E producer
public void pushAll(Iterable<? extends E> src) {
for (E e : src) // out T, 从src中读取数据,producer-extends push(e);
}
要实现popAll(Collection dst)方法,将Stack中的元素依次取出add到dst中,如果不用通配符实现:
同样地,假设有一个实例化Stack的对象stack,dst为Collection; 调用popAll方法是会发生type mismatch错误,因为Collection不是Collection的子类型。 因而,应改为:
// Wildcard type for parameter that serves as an E consumer
public void popAll(Collection<? super E> dst) {
while (!isEmpty())
dst.add(pop()); // in T, 向dst中写入数据, consumer-super
}
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(); // in T, 写入dest数据
ListIterator<? extends T> si=src.listIterator(); // out T, 读取src数据
for (int i=0; i<srcSize; i++) {
di.next();
di.set(si.next());
}
}
}