文章目录
-
-
- 泛型的不可变性
- 协变(Covariance)
- 逆变(Contravariance)
- 何时使用协变和逆变?
-
- [PECS原则:Producer Extends, Consumer Super](#PECS原则:Producer Extends, Consumer Super)
- 总结
- 推荐阅读文章
-
在Java中,泛型(Generics)为编写灵活、可重用的代码提供了很大便利。然而,泛型与继承的关系比较复杂,尤其是涉及 协变(Covariance) 、**逆变(Contravariance) 以及 不可变性(Invariance)**时,这些概念可能让很多开发者感到困惑。
本文将深入探讨Java泛型中的协变、逆变和不可变性,解释它们的含义、使用场景以及如何通过实际代码来理解这些概念。
泛型的不可变性
首先,我们从不可变性 开始,这也是理解协变和逆变的基础。在Java中,泛型是不可变的(Invariance),这意味着即使两个类之间存在继承关系,它们的泛型类型之间却没有这种继承关系。
示例:泛型的不可变性
假设我们有如下的类结构:
java
class Animal {}
class Cat extends Animal {}
class Dog extends Animal {}
Cat
是Animal
的子类,但泛型并不会自动继承,意味着List<Cat>
和List<Animal>
之间没有直接的继承关系。来看下面的代码:
java
List<Cat> cats = new ArrayList<>();
List<Animal> animals = cats; // 编译错误
虽然Cat
是Animal
的子类,但List<Cat>
不能被赋值给List<Animal>
,因为泛型是不可变的。这是为了确保类型安全,避免在操作泛型类型时出现不一致的类型错误。
为什么Java泛型是不可变的?
如果Java允许泛型类型之间的继承,会带来类型安全问题。假设我们可以将List<Cat>
赋值给List<Animal>
:
java
List<Cat> cats = new ArrayList<>();
List<Animal> animals = cats; // 假设可以
animals.add(new Dog()); // 向 cats 添加 Dog 对象
这时,我们往cats
集合中添加了Dog
对象,导致集合内出现了不属于Cat
类型的对象。为了防止这种潜在的类型错误,Java通过禁止泛型的继承关系,确保了类型安全。
如何解决泛型不可变性?
为了解决不可变性带来的限制,Java提供了通配符(Wildcard) ,即? extends
和? super
。这两个通配符分别实现了泛型的协变 和逆变,提供了一定程度上的灵活性。
协变(Covariance)
协变 允许我们使用某个类型的子类型。通俗来说,协变意味着你可以将子类的集合看作是父类的集合。在Java中,协变是通过? extends T
来实现的。
示例:协变的使用
java
List<? extends Animal> animals = new ArrayList<Cat>();
这段代码表示animals
可以引用List<Cat>
,即Cat
作为Animal
的子类可以兼容。但需要注意的是,协变集合是只读的,你不能向协变的集合中添加任何元素:
java
animals.add(new Animal()); // 编译错误
因为编译器不能确定你添加的对象是否与集合的实际类型兼容,Java阻止了这种操作。协变只允许读取操作,比如你可以遍历集合:
java
Animal animal = animals.get(0); // 可以读取
协变的总结:
- 使用
? extends T
实现协变。 - 允许读取集合中的元素。
- 不允许向集合中添加元素。
逆变(Contravariance)
逆变 允许我们使用某个类型的父类型。逆变意味着你可以将父类的集合看作是子类的集合使用。在Java中,逆变通过? super T
来实现。
示例:逆变的使用
java
List<? super Cat> cats = new ArrayList<Animal>();
在这个例子中,cats
可以是Animal
或其任何子类(如Cat
)。你可以向这个集合中添加Cat
或Cat
的子类的对象:
java
cats.add(new Cat()); // 允许
cats.add(new SiameseCat()); // 允许,SiameseCat 是 Cat 的子类
但是当你从集合中读取时,编译器只能确定这个对象是Animal
或更泛化的Object
,而不能确保它是具体的Cat
或SiameseCat
:
java
Cat cat = cats.get(0); // 编译错误,无法确定类型
逆变的总结:
- 使用
? super T
实现逆变。 - 允许向集合中添加元素。
- 读取集合时,只能作为
Object
或父类型读取。
何时使用协变和逆变?
- 协变适用于只需要从集合中读取数据的场景,比如遍历或处理集合中的元素。
- 逆变适用于需要向集合中写入数据的场景,比如往集合中添加元素。
你可以通过以下简单的原则来记住这两者的用法:
PECS原则:Producer Extends, Consumer Super
- Producer Extends :如果你只打算从集合中读取数据,使用
? extends
,即"生产者"模式。 - Consumer Super :如果你打算往集合中添加数据,使用
? super
,即"消费者"模式。
总结
Java泛型中的不可变性 确保了类型安全,防止了父子类泛型类型间不安全的操作。通过使用通配符? extends
和? super
,我们可以在需要时实现协变 和逆变,以便在处理泛型类型时提供更多的灵活性。
- 协变 通过
? extends
允许读取数据,但不允许修改。 - 逆变 通过
? super
允许写入数据,但限制了读取的灵活性。
理解并合理使用协变、逆变以及不可变性,将帮助你在日常编程中避免潜在的类型错误,并提升代码的灵活性与安全性。
希望能帮助你在使用Java泛型时更加得心应手。如果你有更多问题或经验分享,欢迎在评论区留言讨论!
推荐阅读文章
1、使用 Spring 框架构建 MVC 应用程序:初学者教程
2、有缺陷的 Java 代码:Java 开发人员最常犯的 10 大错误
3、如何理解应用 Java 多线程与并发编程?
4、Java Spring 中常用的 @PostConstruct 注解使用总结
5、线程 vs 虚拟线程:深入理解及区别
6、深度解读 JDK 8、JDK 11、JDK 17 和 JDK 21 的区别
7、10大程序员提升代码优雅度的必杀技,瞬间让你成为团队宠儿!
8、"打破重复代码的魔咒:使用 Function 接口在 Java 8 中实现优雅重构!"
9、Java 中消除 If-else 技巧总结
10、线程池的核心参数配置(仅供参考)
11、【人工智能】聊聊Transformer,深度学习的一股清流(13)
12、Java 枚举的几个常用技巧,你可以试着用用
13、如何理解线程安全这个概念?