Java泛型本身是不变的,协变(? extends T)和逆变(? super T)通过通配符实现:前者用于只读(Producer Extends),后者用于只写(Consumer Super),均在编译期保障类型安全。

Java 泛型本身是**不变的(invariant)**,也就是说 List<String> 和 List<Object> 完全无关,不能互相赋值。协变与逆变不是泛型类型自身的属性,而是通过**通配符(wildcard)** 显式引入的“安全型变”机制——它不改变泛型类本身的不变性,而是在使用时(比如方法参数、变量声明)放宽类型约束,同时保证类型安全。
协变:用 ? extends T 表示“只读”场景
协变对应生产者(Producer),即你从容器中「取」数据,希望拿到更通用的类型。
-
? extends T表示“某个 T 的子类型”,上限是 T,下限未知(可能是 T 本身,也可能是任意子类) - 可以安全地 get(),返回类型是
T(编译器能保证不会越界) - 但不能 add()(除了
null),因为编译器不知道具体是哪个子类,加进去可能破坏类型一致性 - 典型例子:
List<? extends Number> nums = new ArrayList<Integer>();✅
你可以调用nums.get(0),结果是Number;但nums.add(3.14)❌ 编译失败
逆变:用 ? super T 表示“只写”场景
逆变对应消费者(Consumer),即你向容器中「放」数据,希望接受更宽泛的输入。
-
? super T表示“某个 T 的父类型”,下限是 T,上限是 Object - 可以安全地 add(T),因为任何
T或其子类对象,都能放进T的父类型容器中 - 但 get() 只能返回
Object(最宽泛的类型),因为编译器无法确定具体是什么父类 - 典型例子:
List<? super Integer> ints = new ArrayList<Number>();✅
你可以ints.add(42)或ints.add(new Long(100))❌(Long 不是 Integer 子类,但 Integer 是 Number 子类,所以 add(Integer) ✅);
而Object o = ints.get(0)✅,但Integer i = ints.get(0)❌ 编译失败
记住 PECS 原则:Producer Extends, Consumer Super
这是理解协变/逆变最实用的口诀:
立即学习“Java免费学习笔记(深入)”;
- 你要 往外拿(produce) 元素 → 用
? extends T(协变)→ 安全读取 - 你要 往里塞(consume) 元素 → 用
? super T(逆变)→ 安全写入 - 如果既要读又要写,就别用通配符,直接用具体类型,比如
List<T>
为什么数组是协变但泛型不是?
Java 数组在运行时保留类型信息,所以 String[] 是 Object[] 的子类型(协变),但这也导致了运行时 ArrayStoreException 风险;而泛型为避免此类隐患,默认设计为不变,并把型变控制权交给开发者——通过通配符显式、安全地启用协变或逆变,所有检查都在编译期完成。


















