逆变与协变

本文详细介绍了Java中泛型的协变和逆变概念,通过实例解释了? extends 和 ? super 的使用场景。PECS原则(Producer-Extends, Consumer-Super)是《Effective Java》中推荐的使用通配符的策略,用于解决在生产者和消费者角色中泛型类型的使用问题。在作为生产者时使用extends,作为消费者时使用super,确保了代码的类型安全。
摘要由CSDN通过智能技术生成

转载:摘抄自《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()); 
			} 
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值