1.2 泛型与型变:生产者、消费者和运行时表示
工坊里的容器既要复用算法,又要阻止错误类型被写进去;继承关系一进入读写接口就不再直观。
泛型让容器和算法保留元素类型,但一旦类型参数既能被读出又能被写入,子类型关系就必须更谨慎。
可变容器通常是不变的
假设允许 Box<Cat> 当作 Box<Animal>:
cats: Box<Cat>
animals: Box<Animal> = cats
animals.set(Dog)
cat = cats.get() ← 现在取出了 Dog所以同时读写的 Box<T> 对 T 通常 invariant。
只生产 T 的接口可以 covariant:若 Cat <: Animal,Producer<Cat> 可当作 Producer<Animal>。只消费 T 的接口可以 contravariant:能处理任意 Animal 的消费者当然能处理 Cat。
Java 使用 use-site variance:
List<? extends Animal> source; // 可安全读作 Animal,不能随意写
List<? super Cat> sink; // 可写入 Cat,读出只能视为 ObjectPECS 是记忆法:Producer Extends, Consumer Super。它不是语法规则的替代;一个参数若既读又写,通常保持精确不变类型。
Array covariance 是带运行时检查的历史选择
Java 数组允许 Cat[] 赋给 Animal[],但写入 Dog 时抛 ArrayStoreException。数组在运行时知道组件类型,而泛型 List<Cat> 不允许同样赋值,在编译期阻止问题。
这说明“语言允许赋值”不代表完全静态安全;有些兼容性把错误推迟到运行时。
Erasure 与 Reification 是实现选择
Java 泛型主要通过 erasure 编译:参数化类型映射到原始类/边界,编译器插入 cast,并在需要保持多态时生成 bridge method。结果是 List<String> 与 List<Integer> 通常共享同一个运行时 class,部分类型参数不可反射。
因此不能创建 new T()、new T[],也不能直接测试 x instanceof List<String>。需要运行时类型时,可显式传 Class<T>、类型令牌或使用 reified generic 的语言机制。
Rust/C++ 常对具体类型实例化专用代码(monomorphization),能优化值布局和静态分派,但可能增加编译时间与代码体积。动态字典/type-class translation 则在运行时传递能力实现。没有一种策略在所有场景都更快。
Bounds 表达算法真正需要的能力
static <T extends Comparable<? super T>> T max(List<? extends T> xs)这个签名说明:输入生产 T;T 能与自己或其上层类型比较。复杂约束的目的不是炫技,而是把实现依赖写进接口,让调用方和编译器都能检查。
过度通用会产生难懂错误与 API。若业务只有三个明确类型,封闭代数数据类型可能比十层泛型更清晰。
泛型 API 检查
- 类型参数是生产、消费还是双向使用?
- 运行时是否需要知道 T,当前实现保留多少类型信息?
- 是否有 unchecked cast,它的安全不变量在哪里证明?
null、空集合和异常怎样进入类型契约?- 通用性是否真的带来复用,还是把领域约束藏进文档?
下一课用 sum/product types 与 exhaustive matching,让“不合法状态不可表示”从口号变成数据建模方法。
参考资料
- Oracle, JLS: Type Erasure
- Oracle, JLS: Subtyping
- Rust, Monomorphization