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 泛型主要通过类型擦除编译:参数化类映射到原始类,类型变量映射到最左边界;编译器插入转换,并按需生成桥接方法(bridge method)。两个普通 ArrayList 对象即使分别声明为存放 String 和 Integer,运行时仍是同一个类。声明元数据可以保留泛型签名,但无法对任意列表对象调用 getClass() 就读出元素类型实参。
不能直接写 new T() 或 new T[]。若 x 的静态类型是 Object,x instanceof List<String> 也不合法,因为它无法检查被擦除的元素类型。Java 16 起允许部分无需未检查转换的参数化测试,例如把 List<Integer> 测试为 ArrayList<Integer>;运行时仍只检查类,不逐个检查元素。API 需要显式运行时类型信息时,可传入 Class<T>、工厂或类型令牌。
Rust/C++ 常对具体类型实例化专用代码(monomorphization),能优化值布局和静态分派,但可能增加编译时间与代码体积。字典传递(dictionary passing)把所需操作的实现作为额外参数传入,编译器也可能对它专门化或内联。没有一种策略在所有场景都更快。
Bounds 表达算法真正需要的能力
static <T extends Comparable<? super T>> T max(List<? extends T> xs)这是方法签名示意:输入提供可视为 T 的值;T 提供的比较操作能接受 T 或其父类型。约束把实现所需能力写进接口,供调用方和编译器检查。
过度通用会产生难懂错误与 API。若业务只有三个明确类型,封闭代数数据类型可能比十层泛型更清晰。
动手区分编译期与运行期检查
把下面代码保存为 VarianceDemo.java,用 JDK 17 或更高版本运行 java VarianceDemo.java。前面的短 Java 片段只展示声明与签名,并非独立程序。
import java.util.ArrayList;
import java.util.List;
class VarianceDemo {
public static void main(String[] args) {
List<Integer> integers = new ArrayList<>(List.of(1, 2));
List<? extends Number> source = integers;
List<? super Integer> sink = new ArrayList<Number>();
sink.add(source.get(0).intValue());
System.out.println(sink.get(0));
// source.add(3);
// List<Number> numbers = integers;
Number[] array = new Integer[] {1};
try {
array[0] = 1.5;
} catch (ArrayStoreException expected) {
System.out.println(expected.getClass().getSimpleName());
}
Object unknown = integers;
System.out.println(unknown instanceof List<?>);
// System.out.println(unknown instanceof List<Integer>);
System.out.println(integers instanceof ArrayList<Integer>);
}
}四行输出依次为 1、ArrayStoreException、true、true。分别取消每条注释:通过生产者视图添加元素、把 List<Integer> 赋给 List<Number>、把未知对象测试为 List<Integer>,都应编译失败。数组写入则能通过编译,在运行时失败。编译器和运行时各管一段,谁也没让那个 Double 悄悄变成 Integer。
泛型 API 检查
- 类型参数是生产、消费还是双向使用?
- 运行时是否需要知道 T,当前实现保留多少类型信息?
- 是否有 unchecked cast,它的安全不变量在哪里证明?
null、空集合和异常怎样进入类型契约?- 通用性是否真的带来复用,还是把领域约束藏进文档?
下一课用 sum/product types 与 exhaustive matching,让“不合法状态不可表示”从口号变成数据建模方法。
参考资料
Oracle, JLS: Type Erasure
Oracle, JLS: Subtyping
Rust, Monomorphization
Oracle, JLS: The instanceof Operator