PECS原则指出:只读用? extends T(生产者),只写用? super T(消费者),读写混合则改用泛型方法;关键看参数在方法中是取数据还是存数据,而非继承关系。

PECS 原则是 Java 泛型中使用通配符的实用指南:只读用 ? extends T(生产者),只写用 ? super T(消费者)。它不依赖类继承图,而取决于你对泛型参数的实际操作——是往外取数据,还是往里塞数据。
只读集合:用 ? extends T 安全取值
当你遍历、获取、处理集合内容,但不添加新元素时,这个集合就是“生产者”。用 ? extends T 表示它产出的每个元素都能安全当作 T 或其父类型使用。
- ✅ 允许:
list.get(0)、for (T item : list)、调用T定义的方法(如item.getName()) - ❌ 禁止:
list.add(...)(连new T()都不行),因为编译器无法确认容器能接受哪种具体子类型 - ? 示例:
void printFruits(List<? extends Fruit> fruits)可接收ArrayList<Apple>或LinkedList<Banana>,遍历时都能调用Fruit的方法
只写集合:用 ? super T 安全写入
当你向集合中添加数据,且这些数据是 T 或它的子类,但基本不从中读取(或只按 Object 处理)时,这个集合就是“消费者”。用 ? super T 表示它至少能容纳所有 T 类型的对象。
你接手一个需求时,是不是经常这样:"改一个小字段 → 结果把不相关的代码也改了"、"需求说得很模糊 → 做着做着发现跑偏了"、"数据库加个字段 → 上线才发现漏了数据兼容"、"做了个新功能 → 一个月后自己都忘了当时怎么设计的"、"写到一半被中断 → 再回来不知道从哪继续";这个 Skill 就是专门解决这些问题的。 它会自动探测你的项目环境(JDK、Spring Boot、ORM、数据库、前端框架),记在
- ✅ 允许:
list.add(new T())、list.add(new SubclassOfT())、addAll(...) - ❌ 禁止:把
list.get(0)直接强转为T使用(只能赋给Object),因为实际可能是List<Object>或List<Serializable> - ? 示例:
void collectStudents(Collection<? super Student> target)可接收ArrayList<Student>、LinkedList<Person>,甚至HashSet<Object>
读写混合场景:避免通配符,改用泛型方法
如果一个方法既要从集合读,又要往里写(比如交换两个位置的元素、过滤并修改),通配符无法兼顾安全读与安全写。这时应放弃通配符,改用带类型参数的泛型方法。
- ❌ 不要写:
void swap(List<?> list, int i, int j)—— 读写都受限,只能操作Object - ✅ 推荐写:
<T> void swap(List<T> list, int i, int j)—— 类型一致,读写都安全 - ? 同理,通用拷贝方法应声明为:
public static <T> void copy(List<? extends T> src, List<? super T> dest),上下游各守其界
别被继承关系带偏,盯住变量角色
判断用 extends 还是 super,关键不是“Apple 是 Fruit 的子类”,而是问自己:“这个参数在方法里是拿来干什么的?”
- 如果方法签名是
void process(List<? extends Fruit> input),且只调用get()和getName()→ 它是生产者 → 用extends - 如果方法签名是
void addAll(List<? super Apple> basket, List<Apple> apples),且只调用basket.add(...)→ 它是消费者 → 用super - 同一个类的不同字段,可能一个用
extends,另一个用super,取决于各自用途

















