Java泛型的协变与逆变

本文深入探讨了Java中泛型的协变和逆变特性,通过实例解释了为什么数组是协变的,而泛型是不变的。文章介绍了如何使用extends和super关键字来实现消费场景的协变和生产场景的逆变,并分析了Collection接口中相关方法的源码,以说明这些特性的实际应用。最后,强调了解这些知识对于提高代码质量和安全性的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

从面向对象说起
Java作为一门面相对象的语言,当然是支持面相对象的三大基本特性的,反手就蹦出三个词:封装、继承、多态。

我们假设有三个类,动物、猫、狗。父类是动物Animal,有两个子类猫Cat和狗Dog。

那在Java中或其它任何支持面相对象的语言中,子类可以把引用赋值给父类。下面这段代码没有任何问题:

Animal animalOne = new Cat();
Animal animalTwo = new Dog();
1
理论上来说,一只猫是一只动物,一只狗也是一只动物,所以这完全是可以理解的。其实,这也是SOLID原则中的“里氏替换原则”的一种体现。

数组的协变
如果一只猫是一只动物,那一群猫是一群动物吗?一群狗是一群动物吗?Java数组认为是的。于是你可以这样写:

Animal[] animals = new Cat[2];
1
这看起来也没有什么问题。但既然都是一群动物了,我往这一群动物中添加一只猫、一只狗,它还是一群动物,这应该是合理的对吧?来看看这段代码:

Animal[] animals = new Cat[2];
animals[0] = new Cat();
// 下面这行代码会抛运行时异常 animals[1] = new Dog();
Animal animal = animal[0];
1
很好,编译没有任何问题。但是一运行,会抛出一个运行时异常:ArrayStoreException。这个异常头顶的注释已经写得很明显了,如果你往数组中添加一个类型不对的对象,就会抛这个异常。它是从JDK 1.0就存在的一个异常。

这么一想,对啊,animals虽然门面上是一个Animal数组,但是它运行时的本质还是一个Cat数组啊,一个Cat数组怎么能添加一个Dog呢?但Java编译器并没有这么智能,而且上述代码在编

译器看来也是合理合法的,所以也就让它编译过了。

所以这种情况,编译器100%过,而运行时100%抛异常,这不是大写的BUG是啥?

如果Cat是Animal的子类型,那么Cat[]也是Animal[]的子类型,我们称这种性质为协变(covariance)。Java中,数组是协变的。

泛型的不变性
在Java 1.5之前,是没有泛型的。那个时候从集合中存取对象都是Object类型,所以每次取出对象后必须进行强转:

List list = new LinkedList(); list.add(123); list.add(“123”); int a = (int)list.get(0); // 下面这段代码会在运行时抛异常 int b = (int)list.get(1); 复制代码
1
如果不小心存入集合中对象类型是错的,会在运行时报强转异常。而1.5提供泛型以后,可以让编译器自动帮助转换,并对代码进行检查,使程序更加安全。

在Java8又加入了泛型的类型推导功能,使用泛型以后,我们的代码看起来变得简洁又安全了:

List list = new LinkedList<>();
list.add(123);

### Java 中的逆变 #### 逆变 (Contravariance) 在Java中,逆变是指允许使用某个的超类来替代该。具体来说,`? super T` 表示可以接受 `T` 类或其任何超类的实例。这一特性主要应用于写入操作,确保能够向集合中添加合适的元素。 例如: ```java List<? super Integer> list = new ArrayList<>(); list.add(1); // 合法, 因为Integer是Number的一个子类 // list.get(0) 返回 Object 类,因为无法确定确切类 ``` 上述代码展示了如何利用逆变机制实现更灵活的数据存储方式[^1]。 #### (Covariance) 相对于逆变而言,则是指当一个作为另一个的子类时所表现出的行为模式。即对于两个不同的实际参数化类A<T1>, B<T2>, 如果存在继承关系使得T1是T2的子类,则可以说A<T1>也是B<T2>的一种特例形式。在Java里我们通常看到的形式就是`<?> extends T`, 它意味着此位置上的类量既可以是具体的`T`也可以是从`T`派生出来的任意子类。 下面是一个简单的例子说明的应用场景: ```java public class Animal {} public class Dog extends Animal {} List<Dog> dogs = Arrays.asList(new Dog()); List<? extends Animal> animals = dogs; for (Animal animal : animals){ System.out.println(animal); } ``` 这里可以看到由于采用了的方式定义列表animals, 所以可以直接赋值给由Dog组成的列表dogs而不必担心编译错误的发生[^3]. #### 不性(Invariance) 值得注意的是,在某些情况下,既不支持也不支持逆变的情况被称为不性。这意味着只有当两个的具体类完全一致的情况下才能互相转换。比如`ArrayList<String>`就不能被当作`ArrayList<Object>`处理,即使String确实是Object的子类之一。 总结起来,通过合理运用这些概念可以帮助开发者编写更加通用且安全高效的程序逻辑结构。同时也能有效减少不必要的强制转所带来的风险以及提高代码可读性和维护便利度。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值