JavaSe笔记----泛型中的协变与逆变

一、协变与逆变的概念

协变与逆变是用来描述类型转换后的继承关系: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

https://blog.csdn.net/yasinshaw/article/details/111878133

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值