Java 泛型:不变性、协变、逆变、泛型擦除与 PECS 原则
近期,笔者在重读 Alibaba 的 p3c 文档《Java开发手册》的第一章编程规约中第二节集合处理的第 12 条时遇到的一个疑惑
其内容指出:
泛型通配符<? extends T>来接收返回的数据,此写法的泛型集合不能使用add方法, 而<? super T>不能使用get方法,两者在接口调用赋值的场景中容易出错。
说明:扩展说一下PECS(Producer Extends Consumer Super) 原则,即频繁往外读取内容的,适合用 <? extends T>,经常往里插入的,适合用<? super T>
因为 Java 泛型类型系统是实现类型安全与代码复用的重要基石,泛型的“不变性、协变、逆变”则是理解泛型容器行为的核心,而“泛型擦除”更是揭示了 Java 泛型的底层机制。因此要想吃透 Java 中泛型相关的部分必须要搞清楚这几个的区别和用法,于是笔者就去 Stack Overflow 社区收集了相关的资料去理解了它们间的区别与联系。现在,笔者可以结合 PECS(Producer Extends, Consumer Super)原则,写出更健壮、更灵活的泛型 API,另外,笔者会对该部分做个简要记录,也会通过代码与关系图解,对泛型来一手深入浅出。
一、泛型类型关系全景图
先用一个泛型类型关系全景图,直观展示泛型类型参数之间的继承与通配符关系:
二、不变性(Invariance)
原理
不变性指:即使A
是B
的子类,Generic<A>
和Generic<B>
之间没有继承关系。这样设计是为了保证泛型容器的类型安全。
示例
List<Number> numList = new ArrayList<Integer>(); // 编译错误
原因:允许上述代码将导致类型不安全。例如,numList.add(3.14)
会破坏ArrayList<Integer>
的类型约束。
三、协变(Covariance)与 PECS
原理
协变允许子类型泛型赋值给父类型泛型变量。Java 通过? extends T
实现协变,常用于只读场景。
代码
List<? extends Number> covariantList = new ArrayList<Integer>();
Number n = covariantList.get(0); // 只读,安全
// covariantList.add(1); // 编译错误
PECS 分析
- Producer Extends:如果你只从集合中取出数据,使用
? extends T
。容器是“生产者”。 - 口诀:Get 允许,Put 禁止。
协变图解
四、逆变(Contravariance)与 PECS
原理
逆变允许父类型泛型赋值给子类型泛型变量。Java 通过? super T
实现逆变,常用于只写场景。
代码
List<? super Integer> contravariantList = new ArrayList<Number>();
contravariantList.add(123); // 只写,安全
// Integer n = contravariantList.get(0); // 编译错误
PECS 分析
- Consumer Super:如果你只向集合中写入数据,使用
? super T
。容器是“消费者”。 - 口诀:Put 允许,Get 禁止。
逆变图解
五、泛型擦除(Type Erasure)
原理
Java 泛型为兼容早期 JVM,在编译阶段会擦除类型参数,所有泛型类型在字节码中都表现为原始类型。类型检查仅在编译时有效,运行时类型信息丢失。
影响
- 不能实例化泛型类型参数(如
new T()
非法)。 - 不能使用泛型数组(如
new List<String>[10]
非法)。 - 运行时类型检查失效。
代码示例
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
System.out.println(list1.getClass() == list2.getClass()); // true
泛型擦除图解
六、PECS 原则总结表
场景 | 泛型写法 | 主要操作 | PECS 原则 | 代码安全性 |
---|---|---|---|---|
不变性 | List<T> | 读/写 | - | 安全 |
协变 | List<? extends T> | 只读 | Producer Extends | 只读安全 |
逆变 | List<? super T> | 只写 | Consumer Super | 只写安全 |
七、实践结论
- 只读场景优先用协变:API 只需要读取数据时,使用
? extends T
,如List<? extends Number>
。 - 只写场景优先用逆变:API 只需要写入数据时,使用
? super T
,如List<? super Integer>
。 - 既读又写用不变性:如
List<T>
,完全类型匹配。 - 理解泛型擦除限制:避免在运行时依赖泛型类型参数。
参考资料
- 《Effective Java》第三版,Item 28, 29
- Java Generics FAQ
- PECS 原则详解