读取数据用 ? extends T(生产者),可接收子类型列表、get() 返回 T 类型,但不可 add(除 null);写入数据用 ? super T(消费者),可安全 add T 类型对象,但 get() 只能返回 Object。

读取数据时用 ? extends T
当你只从集合里拿东西、不往里放,这个集合就是“生产者”。比如遍历一个水果列表并打印名称,你只关心拿到的对象能当 Fruit 用就行,不管它具体是苹果还是香蕉。
这时声明为 List<? extends Fruit> 是安全的:
- 可以接收 List<Apple>、List<Banana> 等子类型列表
- 调用 get() 返回的是 Fruit 类型(或其子类,自动向上转型)
- 但不能调用 add()(除了 null),因为编译器不知道底层到底是哪种具体子类列表,写入可能破坏类型一致性
写入数据时用 ? super T
当你只往集合里塞东西、不从中读取具体类型,这个集合就是“消费者”。比如一个通用的添加方法,要能接受任何能装下 Integer 的列表——无论是 List<Integer>、List<Number> 还是 List<Object>。
这时声明为 List<? super Integer> 就合适:
- 可以安全调用 add(new Integer(42)),因为所有这些列表都保证能容纳 Integer
- 但调用 get() 只能返回 ,不能再直接当 Integer 用——因为你无法确定取出来的是不是 Integer(比如列表实际是 List<Number>,里面可能存了 Double)
别混淆“能放什么”和“能拿什么”
很多人卡在“为什么 ? super Integer 能放 Integer 却不能读成 Integer”。关键在于:通配符约束的是**操作的安全边界**,不是运行时类型。
- ? extends Fruit:读取有上限(最多是 Fruit),写入无保障 → 可读不可写
- ? super Integer:写入有下限(至少能装 Integer),读取无保障 → 可写不可读具体类型
真实场景中的典型用法
Java 标准库大量使用 PECS。比如:
- Collections.copy(dest, src):dest 声明为 List<? super T>(要往里写),src 声明为 List<? extends T>(只往外读)
- Stream.of(T... values) 返回 Stream<T>,但 Stream.concat(a, b) 中两个参数都是 Stream<? extends T>,因为只消费元素做合并,不修改原流








