一、协变与逆变的概念
协变与逆变是用来描述类型转换后的继承关系:A、B表示类型,f()表示类型转换,A<=B表示A是B的子类,那么则有如下关系:
f()表示协变:当A<=B时,则f(A)<=f(B)成立
f()表示逆变:当A<=B时,则f(A)>=f(B)成立
f()表示不变:当A<=B时,则f(A)与f(B)没有继承关系
二、数组的协变
我们假设有三个类:动物,猫,狗,其中动物为父类,猫和狗为动物的两个子类
在Java中子类是可以把引用指向父类的,则如下代码是没有任何问题的:
Animal cat=new Cat();
Animal dog=new Dog();
理论上一只猫是一个动物,一只狗是一个动物,这是很好理解的,那么一群猫是一群动物吗?一群狗是一群动物吗?在Java中是可以这样认为的,于是你可以这样写
Animal[] animals=new Cat[2];
这样写看起来是没有问题的,但既然是一群动物,那么猫和狗混合在一起那是不是也是一群动物?在理论上来说这是不是很明显,那么我们来看这段代码
Animal[] animals=new Cat[2];
animals[0]=new Cat();
//下面这行代码会报运行时异常
animals[1]=new Dog();
Animal animal=animals[0];
很好,编译没有任何问题。但是一运行,会抛出一个运行时异常:ArrayStoreException。这个异常头顶的注释已经写得很明显了,如果你往数组中添加一个类型不对的对象,就会抛这个异常。它是从JDK 1.0就存在的一个异常。
这么一想,对啊,animals虽然门面上是一个Animal数组,但是它运行时的本质还是一个Cat数组啊,一个Cat数组怎么能添加一个Dog呢?但Java编译器并没有这么智能,而且上述代码在编译器看来也是合理合法的,所以也就让它编译过了。所以这种情况,编译器100%过,而运行时100%抛异常,这不是大写的BUG是啥?
如果Cat是Animal的子类型,那么Cat[]也是Animal[]的子类型,我们称这种性质为「协变」(covariance)。「Java中,数组是协变的」。但从上面这个例子也可以看出协变是有写入安全问题的。
三、泛型的不变性
在Java1.5之前是没有泛型这个概念的,那时从集合中获取对象都是Object类型,所以在获取后都要进行强制类型转换
List list = new LinkedList();
list.add(123);
list.add("123");
int a = (int)list.get(0);
// 下面这段代码会在运行时抛异常
int b = (int)list.get(1);
《Effective Java》中,第28条(第三版)说,列表优先于数组。Java在使用列表+泛型时,吸取了上面数组的教训。前面提到,Java中数组是协变的,所以会有些问题。而「Java中的泛型是不变(invariance)的」,也就是说,List<Cat>并不是List<Animal>的子类型。
四、消费场景的协变
比如我有一个Animal的集合,我不管其中存储的具体的类型,但我从这个集合中取出一个元素,那么这个元素一定是Animal的子类,这就是一种典型的消费场景,从集合中取出一个元素来消费。在消费场景中,Java提供了通配符和extends来实现泛型的协变
List<? extends Animal> animals=new LinkedList<Cat>();
// 以下四行代码都不能编译通过
// animals.add(new Dog());
// animals.add(new Cat());
// animals.add(new Animal());
// animals.add(new Object());
// 可以添加null,但没意义
animals.add(null);
// 可以安全地取出来
Animal animal = animals.get(0);
也就是说,虽然因为泛型的不变性,List<Cat>并不是List<Animal>的子类,但Java通过其他方式来支持了泛型的协变,List<Cat>
是List<? extends Animal>
的子类型。与此同时,Java在编译器层面「通过禁止写入的方式,保证了协变下的安全性」。
五、生产场景的逆变
假如希望有一个子类可以写入Animal及其子类,就可以通过通配符和super来实现
/ 下面这行代码编译不通过
// List<? super Animal> animals = new LinkedList<Cat>();
// 下面都是OK的写法
// List<? super Animal> animals = new LinkedList<Object>();
// List<? super Animal> animals = new LinkedList<Animal>();
// 等价于上面一行的写法
List<? super Animal> animals = new LinkedList<>();
animals.add(new Cat());
animals.add(new Dog());
// 取出来一定是Object
Object object = animals.get(0);
// 这样写是OK的
List<? super Cat> cats = new LinkedList<Animal>();
从这就可以看逆变与协变的性质相反。
简单的总结来说? extends T表示所存储类型都是T及其子类,但是获取元素所使用的引用类型只能是T或者其父类。使用上限通配符实现向上转型,但是会失去存储对象的能力。上限通配符为集合的协变表示
下限通配符 ? super T表示 所存储类型为T及其父类,但是添加的元素类型只能为T及其子类,而获取元素所使用的类型只能是Object,因为Object为所有类的基类。下限通配符为集合的逆变表示。
参考文献:https://blog.csdn.net/zy_jibai/article/details/90082239