近期,笔者在重读 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,另外,笔者会对该部分做个简要记录,也会通过代码与关系图解,对泛型来一手深入浅出。


一、泛型类型关系全景图

先用一个泛型类型关系全景图,直观展示泛型类型参数之间的继承与通配符关系:

泛型类型关系全景图.png


二、不变性(Invariance)

原理

不变性指:即使AB的子类,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 禁止

协变图解

协变.png


四、逆变(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 禁止

逆变图解

逆变.png


五、泛型擦除(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

泛型擦除图解

泛型擦除.png


六、PECS 原则总结表

场景泛型写法主要操作PECS 原则代码安全性
不变性List<T>读/写-安全
协变List<? extends T>只读Producer Extends只读安全
逆变List<? super T>只写Consumer Super只写安全

七、实践结论

  1. 只读场景优先用协变:API 只需要读取数据时,使用? extends T,如List<? extends Number>
  2. 只写场景优先用逆变:API 只需要写入数据时,使用? super T,如List<? super Integer>
  3. 既读又写用不变性:如List<T>,完全类型匹配。
  4. 理解泛型擦除限制:避免在运行时依赖泛型类型参数。

参考资料

标签: none

添加新评论