2.10 Java帝国之泛型

从集合依赖 Object 和强制转换走到类型参数,把不匹配提前到编译期,并沿擦除、方法推断与继承边界验证 Java 泛型。

学习目标

  • 能沿声明类型参数、编译期检查、类型擦除、运行时转换追踪一个参数化类型
  • 能解释泛型方法的类型推断、上界/下界与泛型和继承之间的可替换边界
  • 能在正常、边界和故障场景中回答:为什么不能把一组 Integer 当成一组 Number 写入 Double

2.10 Java帝国之泛型

本页依据刘欣《码农翻身》(2018 年第 1 版)及出版社公开书目信息,独立重构 2.10 Java帝国之泛型。正文、代码、图示、实验和练习都是本课程重新设计的教学材料,不复制原书正文、插图、练习答案或代码。

先想象仓库有两种标签:一种只写“箱子”,取货时才打开确认;另一种在入库时就写明“只能装整数”。前者看起来灵活,却把错误留到发货时;后者要求入口更清楚,但让后面的搬运可以少做猜测。

本章解决的是“容器和函数如何提前声明它们接受什么”。如果没有这层声明,错误会穿过编译阶段,直到运行时转换才爆出来;如果声明过度,又会把本来安全的读取或复用拒绝掉。泛型的验收因此必须同时看编译期边界、运行时擦除和继承可替换性。

泛型链:先在编译期约束,再面对擦除边界类型参数改善源代码合同,但不让运行时凭空获得完整参数信息1声明参数T / 实参静态证据2编译检查约束与推断主要安全边界3擦除边界上界签名运行证据4运行转换桥接/检查运行证据类型实参的继承,不等于参数化容器可以任意替换
专属图示:把源代码检查、类型擦除和运行时转换放到同一条证据链。

三个会让泛型失真的陷阱

六个目录节点到类型证据

2.10 Java帝国之泛型

总合同是:在编译期拒绝不满足的类型关系,在运行时只依赖擦除后仍然存在的边界,并让读取/写入的位置决定可接受的泛化程度。最小证据要包含源代码、编译结果、擦除后的签名和重置后的同一输入。

新王登基

描述的是从“所有东西先当作 Object”到“容器先声明元素合同”的变化。类型参数不是装饰语法:它让集合的加入、取出和组合在编译阶段拥有同一份约束。

C 使者

代表编译期检查者。若把 Double 放入只接受 Integer 的容器,C 使者应在构建阶段拒绝;若编译器放行了原始类型转换,风险就转移到运行时,应在证据中标出这条逃逸路径。

泛型实现

需要区分源代码视角和字节码视角。源代码用参数化类型获得检查,编译后类型参数通常擦除到上界;重写方法的返回类型变化可能由编译器生成桥接方法维持多态调用。

泛型链:先在编译期约束,再面对擦除边界类型参数改善源代码合同,但不让运行时凭空获得完整参数信息1声明参数T / 实参静态证据2编译检查约束与推断主要安全边界3擦除边界上界签名运行证据4运行转换桥接/检查运行证据类型实参的继承,不等于参数化容器可以任意替换
专属图示:把源代码检查、类型擦除和运行时转换放到同一条证据链。

泛型方法

把同一个算法复用于不同类型,但推断不是魔法。先列出每个参数对类型变量施加的约束,再看返回目标是否收紧结果;空值或通配符不足以提供可靠约束时,要显式传入类型信息。

泛型和继承

要避免一个危险直觉:Integer 是 Number 的子类型,不代表“Integer 的列表”也是“Number 的列表”。允许任意写入会破坏容器原有合同;上界适合读取,下界适合写入,具体类型适合同时读写。

最小泛型合同

static <T> T first(List<T> values) {
  if (values.isEmpty()) throw new IllegalArgumentException("empty");
  return values.get(0);
}
 
List<Integer> numbers = List.of(1, 2, 3);
Integer value = first(numbers);

合同的关键不是 T 这个字母,而是同一次调用中输入元素类型、返回类型和空集合策略彼此一致。若把参数化信息擦掉再强制转换,必须在测试中标记这种不安全入口,而不能把它当成泛型已经保证安全。

五步复核一条泛型链

分步1 / 5

1. 声明类型参数并固定输入

先写出类/接口/方法的类型变量、实参和空值策略。基线使用一组 Integer;只改变一个条件,例如将写入值改成 Double,预期是编译期拒绝,而不是运行时才报错。

泛型链:先在编译期约束,再面对擦除边界类型参数改善源代码合同,但不让运行时凭空获得完整参数信息1声明参数T / 实参静态证据2编译检查约束与推断主要安全边界3擦除边界上界签名运行证据4运行转换桥接/检查运行证据类型实参的继承,不等于参数化容器可以任意替换
专属图示:把源代码检查、类型擦除和运行时转换放到同一条证据链。

Lab

泛型读写与擦除实验

只改变参数化类型、读写方向或入口安全性,观察错误是在编译期还是运行时出现。

一组 Integer 只读写 Integer

List<Integer> → add(Integer) → get() = Integer → compile ok

判定

accept:输入、返回和副作用使用同一类型合同

当前样本:精确参数;保存类型参数、编译输出、警告、擦除边界和转换位置。

正常、边界与故障证据矩阵

泛型证据矩阵:编译合同要能解释运行结果正常样本看类型稳定,边界样本看读写规则,故障样本看转换位置观察项正常边界故障声明精确 T空值/冲突原始类型读写同型读写上界/下界错误写入实现检查通过擦除边界桥接转换结果类型稳定显式拒绝运行时异常先标出类型约束,再定位警告、桥接或转换的最早位置
专属图示:把类型关系、读写位置与运行时证据逐行对齐。
样本只改变的变量预期判定必存证据
正常精确参数化类型、匹配实参编译通过,读取结果保持类型源码、编译输出、返回类型
边界上界/下界或空值约束只允许合同支持的读写,空值显式处理约束列表、错误位置、调用签名
故障原始类型或未经检查转换警告/拒绝,运行时转换点可定位警告、字节码签名、异常位置

故障诊断:先问错误发生在编译前还是擦除后

  1. 声明侧:核对类型变量、精确实参和空值策略;容器的元素合同不能靠注释补回。
  2. 推断侧:列出参数位置、目标类型、上界和下界;冲突约束应在编译期暴露。
  3. 继承侧:区分类型实参继承与参数化类型可替换性;先判断操作是读、写还是读写。
  4. 运行侧:查擦除后的边界、桥接方法和检查转换;原始类型造成的警告要追到第一次不安全转换。

如果一段代码编译失败,先读错误位置和推断约束;如果编译通过却运行时失败,优先查原始类型、未经检查转换和桥接方法;如果 API 过于保守,回到读写位置选择合适的上界或下界,而不是把所有参数改成 Object。

术语表

名词解释

本章出现的专业名词,用大白话再讲一遍。

新王登基

用类型参数替代所有元素都先当作 Object 的宽泛容器约定。

C 使者

在编译阶段检查类型参数、实参和边界是否匹配的检查者。

泛型实现

源代码参数化、编译后擦除以及桥接方法组成的实现边界。

泛型方法

方法自己声明类型变量,并从调用参数和返回目标推断具体类型的方法。

泛型和继承

类型实参的继承关系与参数化类型读写可替换性之间的边界。

练习

练习

问题 1: 为什么一组 Integer 不能直接当作一组 Number 传给允许写入 Number 的方法?应该怎样设计只读方法?

问题 2: 一个泛型方法在传入两个不同类型参数时推断失败。你会按什么顺序排查?

问题 3: 修改本页“泛型读写与擦除实验”的故障场景,让它显示一次原始类型造成的运行时转换失败,并说明重置后应恢复哪些证据。

资料与写作方式声明

本章以码农翻身权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

本页小结

  • 类型参数把容器和方法的错误提前到编译期。
  • 推断要看参数位置、目标类型和上下界,不是猜变量名。
  • 类型实参继承不自动带来参数化类型协变。
  • 擦除与桥接解释了运行时仍会出现的转换边界。

读完后的自测问题是:面对“Integer 是 Number 的子类型”这句话,你能否说明为什么它不足以证明一组 Integer 可以写入一组 Number,并指出安全的读写 API 设计?

讨论

评论区加载中…