把握Java泛型的艺术:协变、逆变与不可变性一网打尽

文章目录

在Java中,泛型(Generics)为编写灵活、可重用的代码提供了很大便利。然而,泛型与继承的关系比较复杂,尤其是涉及 协变(Covariance) 、**逆变(Contravariance) 以及 不可变性(Invariance)**时,这些概念可能让很多开发者感到困惑。

本文将深入探讨Java泛型中的协变、逆变和不可变性,解释它们的含义、使用场景以及如何通过实际代码来理解这些概念。


泛型的不可变性

首先,我们从不可变性 开始,这也是理解协变和逆变的基础。在Java中,泛型是不可变的(Invariance),这意味着即使两个类之间存在继承关系,它们的泛型类型之间却没有这种继承关系。

示例:泛型的不可变性

假设我们有如下的类结构:

java 复制代码
class Animal {}
class Cat extends Animal {}
class Dog extends Animal {}

CatAnimal的子类,但泛型并不会自动继承,意味着List<Cat>List<Animal>之间没有直接的继承关系。来看下面的代码:

java 复制代码
List<Cat> cats = new ArrayList<>();
List<Animal> animals = cats;  // 编译错误

虽然CatAnimal的子类,但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)。你可以向这个集合中添加CatCat的子类的对象:

java 复制代码
cats.add(new Cat());  // 允许
cats.add(new SiameseCat());  // 允许,SiameseCat 是 Cat 的子类

但是当你从集合中读取时,编译器只能确定这个对象是Animal或更泛化的Object,而不能确保它是具体的CatSiameseCat

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、如何理解线程安全这个概念?

相关推荐
Scott9999HH1 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
腻害兔1 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社2 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海2 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖3 小时前
JDK 26 新特性详解
java·开发语言
马优晨4 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区6 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大6 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai7 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
维天说7 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json