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

相关推荐
陌小呆^O^5 分钟前
Cmakelist.txt之win-c-udp-server
c语言·开发语言·udp
计算机毕设指导610 分钟前
基于 SpringBoot 的作业管理系统【附源码】
java·vue.js·spring boot·后端·mysql·spring·intellij-idea
Gu Gu Study12 分钟前
枚举与lambda表达式,枚举实现单例模式为什么是安全的,lambda表达式与函数式接口的小九九~
java·开发语言
Chris _data14 分钟前
二叉树oj题解析
java·数据结构
牙牙70520 分钟前
Centos7安装Jenkins脚本一键部署
java·servlet·jenkins
时光の尘26 分钟前
C语言菜鸟入门·关键字·float以及double的用法
运维·服务器·c语言·开发语言·stm32·单片机·c
paopaokaka_luck28 分钟前
[371]基于springboot的高校实习管理系统
java·spring boot·后端
以后不吃煲仔饭40 分钟前
Java基础夯实——2.7 线程上下文切换
java·开发语言
进阶的架构师41 分钟前
2024年Java面试题及答案整理(1000+面试题附答案解析)
java·开发语言
前端拾光者1 小时前
利用D3.js实现数据可视化的简单示例
开发语言·javascript·信息可视化