该用 ? extends T 还是 ? super T 取决于容器角色:读数据(生产者)用 ? extends T,写数据(消费者)用 ? super T,即 PECS 原则;二者不可混用于同一需读写场景,且通配符仅限使用位置,不可用于泛型声明。

什么时候该用 ? extends T 而不是 ? super T
关键看你是「读数据」还是「写数据」。如果只从集合里取元素(比如遍历、查找),用 ? extends T;如果要往集合里塞新对象(比如添加、填充),用 ? super T。
这是 PECS 原则(Producer Extends, Consumer Super)的直接体现——不是语法规定,而是类型安全强制你这么选。
-
List<? extends Number>可以安全地get(0)得到Number,但不能add(new Integer(1))(编译报错:add(capture#1-of ? extends Number)) -
List<? super Integer>可以安全地add(new Integer(1)),但get(0)只能赋给Object(因为上界未知,最安全类型就是Object) - 别试图把两者混用:比如
List<? extends Number>传给一个需要「既能读又能写」的方法,它根本过不了编译
? extends T 的实际使用场景和典型错误
常见于返回值类型,尤其是工具类方法想兼容子类集合又不破坏类型安全时。
例如,一个求最大值的方法:
立即学习“Java免费学习笔记(深入)”;
public static <T extends Comparable<T>> T max(List<? extends T> list) { ... }
这样调用才合法:
-
max(Arrays.asList(new Integer(1), new Integer(2)))✅ -
max(Arrays.asList(new Double(1.0), new Double(2.0)))✅ -
max(Arrays.asList(new Object()))❌(Object不实现Comparable<Object>) -
max(new ArrayList<Number>())❌(Number本身不实现Comparable<Number>,除非显式指定泛型边界)
? super T 在泛型方法参数中的必要性
当你需要把某种类型「喂进去」,且希望方法能接受更宽泛的容器时,? super T 是唯一选择。
典型例子是 Collections.copy() 的签名:
public static <T> void copy(List<? super T> dest, List<? extends T> src)
它允许你把 List<Integer> 复制进 List<Number> 或 List<Object>,但不允许复制进 List<Long>(类型不兼容)。
- 如果参数写成
List<T>,就只能传完全匹配的类型,失去灵活性 - 如果写成
List<? extends T>,目标列表反而无法接收元素(因为它是「生产者」角色) - 运行时不会出错,但编译期就卡死——Java 泛型擦除后靠的就是这些边界约束来守住安全底线
容易被忽略的细节:通配符不能用于泛型类声明或泛型方法定义
? extends T 和 ? super T 只能出现在**使用位置**(变量、参数、返回值、字段类型),不能出现在类或方法的泛型形参列表里。
-
class Box<? extends Number> { }❌ 编译错误 -
<T extends Number> void foo(List<T> list) { }✅ 正确(这是类型参数,不是通配符) -
void bar(List<? extends Number> list) { }✅ 正确(这是通配符使用) - 混淆点在于:IDE 有时会把
? extends T的提示显示得像类型参数,但它本质是「未知具体类型的占位符」,没有名字、不能在方法体内当类型用
真正难的不是记住 extends 和 super,而是每次写泛型代码前,先问自己一句:这个容器,此刻到底是生产者,还是消费者。









