当前位置: 首页 > news >正文

泛型学习——看透通配符?与PECS 法则

简单的泛型理解见上一篇文章:https://blog.csdn.net/2302_78439400/article/details/153181138?spm=1001.2014.3001.5501

接下来继续解读一下通配符与PECS法则

我们先从一个问题开始。假设我们有一个动物园,里面有各种动物。

class Animal { void eat() { System.out.println("Animal eats"); } }
class Dog extends Animal { void bark() { System.out.println("Dog barks"); } }
class Cat extends Animal { void meow() { System.out.println("Cat meows"); } }

现在,我想写一个方法,用来遍历任何一个“装有动物的盘子”(List),并让它们都“吃饭”(调用 eat() 方法)。

你可能会很自然地写出这样的代码:

public void feedAll(List<Animal> animals) {for (Animal animal : animals) {animal.eat();}
}

这看起来没问题。但当你想用它来喂一盘狗的时候,问题就来了:

List<Dog> dogs = new ArrayList<>();
dogs.add(new Dog());
​
feedAll(dogs); // 编译直接报错!

为什么会报错?

这是泛型的一个核心原则:List<Dog> 不是 List<Animal> 的子类!

打个比方:一个“装狗的盘子” (List<Dog>) 肯定不是一个“装任何动物的盘子” (List<Animal>)。为什么?因为你不能往“装狗的盘子”里放一只猫,但“装任何动物的盘子”应该是可以放猫的。为了保证这种类型安全,Java 干脆规定它俩没有任何继承关系。

这就很尴尬了。我的 feedAll 方法难道要为 List<Dog>List<Cat> 各自写一个重载版本吗?那也太蠢了。

为了解决这个问题,通配符 ? 闪亮登场。

1. 上界通配符:? extends T (读取/生产者)

我们可以把方法改成这样:

public void feedAll(List<? extends Animal> animals) {for (Animal animal : animals) {animal.eat();}// animals.add(new Dog()); // 编译报错!
}

List<? extends Animal> 这句话翻译过来就是:“一个列表,它里面的元素类型是未知的(?),但这个未知类型肯定是 Animal 或者 Animal 的某个子类”。

  • List<Dog> 符合这个描述吗?符合,DogAnimal 的子类。

  • List<Cat> 符合这个描述吗?符合,CatAnimal 的子类。

  • List<Animal> 符合这个描述吗?符合,AnimalAnimal 本身。

这样一来,feedAll(dogs) 就能编译通过了。

但是,它也带来一个限制:你不能往 List<? extends Animal> 里添加任何东西null 除外)。

为什么?因为编译器只知道这个列表里装的是“某种 Animal 的子类”,但它不确定到底是哪一种。它可能是 List<Dog>,也可能是 List<Cat>。如果它允许你 add(new Dog()),万一这个列表实际上是 List<Cat> 呢?那就出错了。所以为了绝对安全,编译器干脆禁止了一切添加操作。

总结 ? extends T:

  • 它让方法变得更通用,可以接收 T 及其所有子类的集合。

  • 它通常用于读取数据的场景(我们从 animals 列表里 get 元素出来消费)。我们称这种集合为生产者 (Producer),因为它为我们生产(提供)数据。

2. 下界通配符:? super T (写入/消费者)

我们再看另一个场景。假设我想写一个方法,可以把一只新出生的小猫,添加到任何一个“能装猫的盘子”里。

这个“能装猫的盘子”可能是 List<Cat>,也可能是 List<Animal>,甚至是 List<Object> (因为 Cat 也是 Object)。

如果我们写成 public void addCat(List<Cat> cats),那 List<Animal> 就传不进来了。这时就需要下界通配符:

public void addCat(List<? super Cat> cats) {cats.add(new Cat()); // 完全没问题!
​// Object item = cats.get(0); // 取出来只能当 Object 用
}

List<? super Cat> 这句话翻译过来就是:“一个列表,它里面的元素类型是未知的(?),但这个未知类型肯定是 Cat 或者 Cat 的某个父类”。

  • List<Cat> 符合吗?符合。

  • List<Animal> 符合吗?符合。

  • List<Object> 符合吗?符合。

为什么这次可以 add 了?因为无论这个列表到底是 List<Cat>, List<Animal> 还是 List<Object>,放一只 Cat 进去都是类型安全的。

但是,当你从这个列表里取元素时,编译器为了安全,只能保证取出来的一定是 Object。因为它不确定这个列表到底是哪种父类型,只能给你所有类型的共同祖先 Object

总结 ? super T:

  • 它让方法变得更通用,可以接收 T 及其所有父类的集合。

  • 它通常用于写入数据的场景(我们往 cats 列表里 add 新元素)。我们称这种集合为消费者 (Consumer),因为它消费(接收)我们提供的数据。

PECS 法则

上面这两条,合在一起就是大名鼎鼎的 PECS 法则

Producer-Extends, Consumer-Super

  • 生产者用 Extends:如果你需要一个集合来读取/获取数据(它作为生产者),那么用 ? extends T

  • 消费者用 Super:如果你需要一个集合来写入/添加数据(它作为消费者),那么用 ? super T

  • 既要读又要写:那就不要用通配符,直接用精确类型,比如 List<Animal>

JDK 里的 Collections.copy() 方法就是 PECS 法则的最佳实践: public static <T> void copy(List<? super T> dest, List<? extends T> src)

  • src 是源头,是生产者,我们只会从里面读,所以用 extends

  • dest 是目的地,是消费者,我们只会往里面写,所以用 super


类型擦除 - 泛型的“底层秘密”

有没有想过一个问题:泛型是 JDK 1.5 才有的新功能,那 1.5 之前编译出来的旧 class 文件(字节码),是怎么和 1.5 之后带泛型的代码一起工作的呢?

答案就是类型擦除 (Type Erasure)

核心思想:泛型只存在于编译期,用来给编译器做类型检查。到了运行期,JVM 其实是看不到这些泛型信息的,它们都被“擦除”了。

可以这么理解: List<String>List<Integer> 在写代码和编译的时候,是两种完全不同的类型。但是,一旦编译完成,生成的字节码里,它们都会变回“赤裸裸”的 List,也就是我们“蛮荒时代”的那个 List

编译器在“擦除”前后做了两件重要的事:

  1. 类型检查:在编译时,严格按照你写的泛型(如 List<String>)来检查你的代码。如果你 add 了一个 Integer,编译器直接报错。这是泛型安全的核心。

  2. 自动类型转换:当编译器检查通过后,它在生成字节码时,会偷偷地帮你加上强制类型转换。 比如你的代码是 String s = list.get(0);,生成的字节码实际上可能是 String s = (String) list.get(0);

类型擦除带来的几个重要限制(面试常考):

  1. 不能 new T():你不能在泛型类里写 T data = new T();。因为擦除后,T 会变成 Object,JVM 根本不知道你想 new 的是哪个具体类。

  2. 不能 instanceof List<String>:你不能用 instanceof 来判断一个对象是否属于某个具体的泛型类型。if (myList instanceof List<String>) 是非法的。因为在运行时,JVM 眼里只有 List,没有 <String>。你只能写 if (myList instanceof List)

  3. 不能创建泛型数组:你不能写 List<String>[] array = new List<String>[10];。这也是因为数组在运行时需要知道自己的确切元素类型,但泛型信息被擦除了,这就产生了矛盾。

一句话总结类型擦除: 泛型是编译器给你的一个“君子协定”。它在编译代码时帮你把关,确保类型安全。一旦代码编译通过,它就“卸磨杀驴”,把泛型信息擦掉,换成旧的 Object 和强制转换,让代码能在任何 JVM 上运行。


PECS 法则和“类型擦除”是泛型里最抽象、最底层,但也最重要的两个概念。

  • PECS 指导使用者如何设计出更灵活、更通用的 API

  • 类型擦除 解释了泛型的工作原理及其局限性

补充说明:

即使在 JDK 21 里,类型擦除依然存在吗?

答案是:是的,绝对存在。

这可能是 Java 最著名的“历史包袱”之一。类型擦除机制的核心目的就是为了向后兼容

你想想,在 JDK 1.5 之前,所有的 ArrayList 在字节码层面就是 ArrayList,里面存的都是 Object。如果从 JDK 1.5 开始,编译后的字节码里突然出现了 ArrayList<String> 这种全新的东西,那么老的 JVM(1.5 之前的版本)就会完全不认识,直接抛出错误。

为了让泛型这个新语法能够在所有(包括老的)JVM 上运行,Java 的设计者们才决定采用“编译器做手脚,JVM 无感知”的策略,也就是类型擦除。

所以,直到今天最新的 JDK 版本,为了维护整个 Java 生态的稳定和兼容性,这个机制依然是泛型工作的基石。任何一个 List<String> 在编译后,其字节码中的类型签名依然是 List

http://www.dtcms.com/a/482085.html

相关文章:

  • 跨平台音频IO处理库libsoundio实践
  • 详解云原生!!
  • 网站跟客户端推广怎么做 上软件免费下载
  • JVM - 内存泄露与内存溢出
  • iOS 26 性能测试实战,性能评估、帧率、资源瓶颈 + 多工具辅助测试
  • elasticsearch数据迁移
  • 可以横跨时间轴,分类显示的事件
  • 2.0 轴承的分类与套筒、甩油环作用
  • mvc 网站 只列出目录wordpress速度慢2018
  • 电子商务网站建设一体化教案代运营公司网站
  • 深度学习与大模型技术实战:从算法原理到应用部署
  • YOLO v3:目标检测领域的经典革新与实战指南
  • MATLAB基于GWO(灰狼优化算法)优化LSTM神经网络的分类模型实现。主要功能是通过智能算法自动寻找LSTM的最佳超参数,构建分类模型并对数据进行分类预测
  • 网站的制医院网站建设台账
  • 用python操作mysql之pymysql库基本操作
  • 数据结构 05 栈和队列
  • 01、大模型部署方案与Dify的使用
  • 使用Spring Boot构建消息通信层
  • 山东济南seo整站优化公司对其网站建设进行了考察调研
  • MIPI_CSI22_Xilinx IP
  • 【C++STL :stack queue (一) 】STL:stack与queue全解析|深入使用(附高频算法题详解)
  • DevOps工具链对比,云效 vs TikLab哪一款更好用?
  • Kanass,一款超级轻量且简洁的项目管理工具
  • 如何做企业的网站微信如何开通小程序
  • 【从0开始学习Java | 第20篇】网络编程
  • PetaLinux 工程迁移指南
  • Java面试实战:互联网医疗场景中的JVM调优与Spring Boot应用
  • http环境实现通知
  • 分布式雷达 vs 多基地雷达:同频共振的“合唱团”和“乐队”
  • 手机端-adb脚本自动化-真机版