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

相关推荐
Swift社区6 分钟前
Excel 列名称转换问题 Swift 解答
开发语言·excel·swift
一道微光10 分钟前
Mac的M2芯片运行lightgbm报错,其他python包可用,x86_x64架构运行
开发语言·python·macos
矛取矛求14 分钟前
QT的前景与互联网岗位发展
开发语言·qt
Leventure_轩先生14 分钟前
[WASAPI]从Qt MultipleMedia来看WASAPI
开发语言·qt
向宇it29 分钟前
【从零开始入门unity游戏开发之——unity篇01】unity6基础入门开篇——游戏引擎是什么、主流的游戏引擎、为什么选择Unity
开发语言·unity·c#·游戏引擎
wm104331 分钟前
java web springboot
java·spring boot·后端
smile-yan32 分钟前
Provides transitive vulnerable dependency maven 提示依赖存在漏洞问题的解决方法
java·maven
老马啸西风33 分钟前
NLP 中文拼写检测纠正论文-01-介绍了SIGHAN 2015 包括任务描述,数据准备, 绩效指标和评估结果
java
Earnest~36 分钟前
Maven极简安装&配置-241223
java·maven
皮蛋很白39 分钟前
Maven 环境变量 MAVEN_HOME 和 M2_HOME 区别以及 IDEA 修改 Maven repository 路径全局
java·maven·intellij-idea