通配符与PECS原则
"通配符
?是泛型世界的'外交官'——它让不同类型参数化的泛型类能够互通有无。掌握PECS原则,你就掌握了泛型设计的终极心法。" —— 白歌,飞翔科技架构师
1. 问题的提出:泛型不是协变的
小崔在飞翔科技写了一个方法,用于打印学生列表:
public void printStudents(List<Student> students) {
for (Student s : students) {
System.out.println(s.getName());
}
}
一切正常,直到有一天他需要打印CollegeStudent(Student的子类)列表:
List<CollegeStudent> collegeStudents = new ArrayList<>();
collegeStudents.add(new CollegeStudent("小崔", 22, "清华大学"));
printStudents(collegeStudents); // ❌ 编译错误!
编译错误:The method printStudents(List<Student>) is not applicable for the arguments (List<CollegeStudent>)
小崔困惑了:CollegeStudent extends Student,为什么List<CollegeStudent>不是List<Student>的子类型?
不可协变的原因
虽然CollegeStudent是Student的子类,但List<CollegeStudent>不是List<Student>的子类。为什么?看下面的例子:
// 假设泛型是协变的(实际上不是!)
// List<CollegeStudent> cs = new ArrayList<>();
// List<Student> students = cs; // 假设允许这样赋值
// students.add(new HighSchoolStudent("张三", 16)); // 放入一个非CollegeStudent的Student
// 那么 cs.get(0) 返回的就会是一个 HighSchoolStudent,但 cs 被声明为
// List<CollegeStudent> —— 类型系统被破坏了!
2. 通配符 ?:泛型世界的救星
通配符(Wildcard)用?表示"未知类型",用于在泛型中表达更灵活的类型关系。
通配符三兄弟
| 通配符 | 名称 | 读能力 | 写能力 | 典型场景 |
|---|---|---|---|---|
<?> | 无界通配符 | 读出为Object | 不能写(除null) | List<?> 作为"只读"集合 |
<? extends T> | 上界通配符 | 读出为T | 不能写(除null) | 生产者——从集合中读取T |
<? super T> | 下界通配符 | 读出为Object | 可以写入T及其子类 | 消费者——向集合中写入T |
一句话总结
3. 上界通配符 <? extends T>:生产者
<? extends T>表示"T或T的某个子类型",具体是哪个子类型未知。由此产生可读不可写的特性。
语法与规则
// 方法参数使用上界通配符 —— 支持所有 Student 的子类型列表
public void printStudents(List<? extends Student> students) {
for (Student s : students) { // ✅ 读取:安全,返回类型是Student
System.out.println(s.getName());
}
// students.add(new Student(...)); // ❌ 写入:编译错误!不知道具体是什么子类型
// students.add(new CollegeStudent(...)); // ❌ 同样编译错误!
students.add(null); // ✅ 只有null可以添加
}
为什么不能写? 因为List<? extends Student>可能是List<CollegeStudent>,也可能是List<HighSchoolStudent>。如果允许add(Student),就可能把HighSchoolStudent放入List<CollegeStudent>中——类型不安全。
完整示例:飞翔科技的成绩单系统
/**
* 飞翔科技成绩计算服务
* 使用上界通配符处理各种学生子类型
*/
public class GradeService {
/**
* 计算学生列表的平均成绩
* <? extends Student> 接受 Student 及其任意子类的列表
*/
public static double calculateAverage(List<? extends Student> students) {
if (students.isEmpty()) return 0.0;
double total = 0.0;
for (Student s : students) { // 安全读出为 Student
total += s.getScore();
}
return total / students.size();
}
/**
* 获取成绩最高的学生
*/
public static Student getTopStudent(List<? extends Student> students) {
if (students.isEmpty()) return null;
Student top = students.get(0);
for (Student s : students) {
if (s.getScore() > top.getScore()) {
top = s;
}
}
return top;
}
}
// 测试代码
public class GradeServiceTest {
public static void main(String[] args) {
// CollegeStudent 列表
List<CollegeStudent> collegeStudents = new ArrayList<>();
collegeStudents.add(new CollegeStudent("小崔", 22, "清华大学", 92.5));
collegeStudents.add(new CollegeStudent("白歌", 28, "北京大学", 88.0));
collegeStudents.add(new CollegeStudent("大翔", 35, "浙江大学", 95.5));
// HighSchoolStudent 列表
List<HighSchoolStudent> highSchoolStudents = new ArrayList<>();
highSchoolStudents.add(new HighSchoolStudent("小明", 16, "飞翔中学", 85.0));
highSchoolStudents.add(new HighSchoolStudent("小红", 17, "飞翔中学", 91.0));
// 同一个方法处理两种不同类型的列表
System.out.println("大学生平均成绩:" +
GradeService.calculateAverage(collegeStudents));
System.out.println("中学生平均成绩:" +
GradeService.calculateAverage(highSchoolStudents));
System.out.println("\n大学生最高分:" +
GradeService.getTopStudent(collegeStudents).getName());
System.out.println("中学生最高分:" +
GradeService.getTopStudent(highSchoolStudents).getName());
}
}
运行输出:
大学生平均成绩:92.0
中学生平均成绩:88.0
大学生最高分:大翔
中学生最高分:小红
List<? extends Number> 的读写行为
List<? extends Number> numbers = new ArrayList<Integer>(); // 可以赋值为任何Number子类的列表
Number n = numbers.get(0); // ✅ 读:返回 Number 类型
// Integer i = numbers.get(0); // ❌ 编译错误!不能确定具体是Integer还是Double
// numbers.add(100); // ❌ 编译错误!
// numbers.add(3.14); // ❌ 编译错误!
numbers.add(null); // ✅ 只有 null 可以
4. 下界通配符 <? super T>:消费者
<? super T>表示"T或T的某个超类型",由此产生可写不可读(读只能到Object)的特性。
语法与规则
// 方法参数使用下界通配符 —— 可以向集合中安全地放入 Student 对象
public void addStudents(List<? super Student> students) {
students.add(new Student("小崔", 22)); // ✅ 写入:安全
students.add(new CollegeStudent("白歌", 28, "北大")); // ✅ 子类也可以
// Student s = students.get(0); // ❌ 编译错误!返回类型是 Object(或捕获类型)
Object obj = students.get(0); // ✅ 只能读为 Object
}
为什么能写但不能读? 因为List<? super Student>可能是List<Student>,也可能是List<Object>。写入Student是安全的(Student是Student自身,也是Object的子类),但读取时无法确定返回类型是Student还是Object——最安全的做法是返回Object。
完整示例:飞翔科技的数据收集器
/**
* 飞翔科技数据收集服务
* 使用下界通配符向集合中填充数据
*/
public class DataCollector {
/**
* 将源列表中的元素复制到目标列表
* <? super T> 目标集合是消费者 —— 接受 T 及其超类的列表
* <? extends T> 源集合是生产者 —— 提供 T 及其子类
*/
public static <T> void copyInto(
List<? super T> dest, // 消费者:写入
List<? extends T> src) { // 生产者:读取
for (T item : src) {
dest.add(item); // 安全写入 —— dest 接受 T 及其超类
}
}
/**
* 批量添加学生到集合
* <? super Student> 接受 Student、Object 等超类的集合
*/
public static void addAllStudents(
List<? super Student> target,
Student... students) {
for (Student s : students) {
target.add(s);
}
}
}
// 测试代码
public class DataCollectorTest {
public static void main(String[] args) {
// === 测试 <? super Student> ===
List<Object> objList = new ArrayList<>(); // Object 是 Student 的超类
DataCollector.addAllStudents(objList,
new Student("小崔", 22, 88.0),
new CollegeStudent("白歌", 28, 95.0)
);
System.out.println("Object列表中的元素:");
for (Object obj : objList) {
System.out.println(" " + obj);
}
// === 测试 copyInto ===
List<CollegeStudent> srcList = new ArrayList<>();
srcList.add(new CollegeStudent("大翔", 35, "浙江大学", 92.0));
srcList.add(new CollegeStudent("小崔", 22, "清华大学", 88.0));
List<Student> destList = new ArrayList<>(); // Student 是 CollegeStudent 的超类
DataCollector.copyInto(destList, srcList);
System.out.println("\n复制结果:");
for (Student s : destList) {
System.out.println(" " + s.getName() + " - " + s.getScore() + "分");
}
}
}
运行输出:
Object列表中的元素:
Student{name='小崔', age=22, score=88.0}
CollegeStudent{name='白歌', age=28, college='null', score=95.0}
复制结果:
大翔 - 92.0分
小崔 - 88.0分
5. PECS原则
PECS 是 Josh Bloch 在《Effective Java》中提出的泛型使用原则:
Producer Extends, Consumer Super
| 角色 | 通配符 | 含义 | 操作 |
|---|---|---|---|
| Producer(生产者) | <? extends T> | 从结构中读取T类型数据 | 只读不写 |
| Consumer(消费者) | <? super T> | 向结构中写入T类型数据 | 只写不读 |
判断方法
问自己一个问题:这个参数是用来读(获取数据)还是写(放入数据)?
- 如果只读:用
<? extends T> - 如果只写:用
<? super T> - 如果又读又写:用精确类型
<T>(不要用通配符)
PECS决策流程图
JDK中的PECS经典案例
Collections.copy —— PECS的教科书级应用
// JDK源码中的签名
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
// dest:消费者(写入),用 <? super T>
// src:生产者(读取),用 <? extends T>
for (int i = 0; i < srcSize; i++) {
dest.set(i, src.get(i));
}
}
使用示例:
List<Object> dest = new ArrayList<>(Arrays.asList(null, null, null));
List<Integer> src = Arrays.asList(1, 2, 3);
Collections.copy(dest, src); // Integer 可以放入 List<Object>——安全!
System.out.println(dest); // [1, 2, 3]
Java 8 Stream API
// Predicate —— 生产者(判断条件)
Stream<T> filter(Predicate<? super T> predicate); // 传入 T 或其超类的谓词
// Consumer —— 消费者(执行操作)
void forEach(Consumer<? super T> action); // 接受 T 或其超类的操作
// Function —— 生产+消费
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
// 输入是消费者(super) 输出是生产者(extends)
飞翔科技完整案例:泛型Service层
/**
* 飞翔科技通用的CRUD服务接口
* 展示PECS原则在实际业务中的运用
*/
public interface Service<T> {
/**
* PECS展示:从集合中读取数据进行处理
* items 是生产者 —— 只读取,用 extends
*/
default List<T> filterByCondition(
List<? extends T> items, // 生产者:读取T数据
Predicate<? super T> condition // 消费者:传入T进行判断
) {
List<T> result = new ArrayList<>();
for (T item : items) { // 从 extends 集合安全读取
if (condition.test(item)) { // 用 super 条件判断
result.add(item);
}
}
return result;
}
/**
* PECS展示:向集合中写入数据
* target 是消费者 —— 只写入,用 super
*/
default void collectInto(
List<? super T> target, // 消费者:写入T数据
T... items
) {
for (T item : items) {
target.add(item); // 向 super 集合安全写入
}
}
}
// 测试代码
public class ServiceTest {
public static void main(String[] args) {
Service<Student> studentService = new Service<Student>() {};
// === 测试 filterByCondition ===
List<CollegeStudent> students = new ArrayList<>();
students.add(new CollegeStudent("小崔", 22, "清华", 92.0));
students.add(new CollegeStudent("白歌", 28, "北大", 78.0));
students.add(new CollegeStudent("大翔", 35, "浙大", 95.0));
// 过滤成绩 >= 90 的学生
// students: List<CollegeStudent> 是 List<? extends Student>
// condition: Predicate<Object> > Predicate<Student>,可以用 Predicate<? super Student>
Predicate<Object> highScore = s ->
s instanceof Student && ((Student) s).getScore() >= 90;
List<Student> topStudents = studentService.filterByCondition(students, highScore);
System.out.println("高分学生:");
topStudents.forEach(s -> System.out.println(" " + s.getName() + ": " + s.getScore()));
// === 测试 collectInto ===
List<Object> collector = new ArrayList<>(); // Object 是 Student 的超类
studentService.collectInto(collector,
new Student("新生A", 20, 85.0),
new Student("新生B", 21, 90.0)
);
System.out.println("\n收集结果:");
collector.forEach(System.out::println);
}
}
运行输出:
高分学生:
小崔: 92.0
大翔: 95.0
收集结果:
Student{name='新生A', age=20, score=85.0}
Student{name='新生B', age=21, score=90.0}
6. 无界通配符 <?>
<?>表示"任意类型的泛型"。它与原始类型List不同——List<?>仍然是泛型,编译器会对它进行类型安全检查。
List<?> vs 原始类型 List
| 特性 | List<?> | List(原始类型) |
|---|---|---|
| 读取 | 返回Object | 返回Object |
| 写入 | 不能写(除null) | 可以写任意类型(不安全) |
| 编译检查 | 泛型检查生效 | 泛型检查完全关闭 |
| 赋值安全 | 安全 | 不安全(警告) |
List<?> wildList = new ArrayList<String>();
// wildList.add("hello"); // ❌ 编译错误!不能写入
// wildList.add(123); // ❌ 编译错误!
wildList.add(null); // ✅ 只有 null 可以
Object obj = wildList.get(0); // ✅ 读取为 Object
// —— 对比 ——
List rawList = new ArrayList<String>();
rawList.add("hello"); // ✅ 编译通过(仅警告)
rawList.add(123); // ✅ 编译通过(仅警告)—— 危险!
使用场景
<?>适用于以下场景:
- 方法不依赖具体类型:如
List.size()、List.clear() - 只使用Object方法:如
toString()、equals() - 类型检查:
obj instanceof List<?>
/**
* 不需要知道元素类型就能完成的操作
*/
public class WildcardUtils {
// ✅ 只使用 List 自身方法,不涉及元素类型
public static boolean isEmpty(List<?> list) {
return list == null || list.isEmpty();
}
// ✅ 只需要 Object 方法
public static void printAll(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
// ✅ 安全类型检查
public static boolean isList(Object obj) {
return obj instanceof List<?>;
}
}
7. 通配符捕获
通配符捕获(Wildcard Capture)是编译器的一种智能推断机制:当操作通配符类型时,编译器内部会创建一个"捕获类型"来替代?。
捕获问题
public static void swapFirstTwo(List<?> list) {
// ❌ 以下代码无法编译!
// Object first = list.get(0);
// Object second = list.get(1);
// list.set(0, second); // 编译错误!
// list.set(1, first); // 编译错误!
}
原因:编译器将List<?>的?捕获为一个未知类型#CAP1,而set(int, #CAP1)要求传入的类型必须是#CAP1——但list.get(0)返回的是#CAP1,list.set(0, list.get(1))应该合法才对啊?
问题在于编译器的捕获类型机制不够智能,无法推断出list.get(0)和list.get(1)的返回类型是同一个#CAP1。
解决方案:辅助泛型方法
// ✅ 方案:用泛型方法捕获通配符类型
public static void swapFirstTwo(List<?> list) {
swapHelper(list); // 编译器将 ? 捕获为具体类型 T
}
// 辅助泛型方法 —— T 捕获了通配符 ?
private static <T> void swapHelper(List<T> list) {
T first = list.get(0);
T second = list.get(1);
list.set(0, second); // ✅ 类型安全:T ↔ T
list.set(1, first);
}
完整验证示例
public class WildcardCaptureDemo {
/**
* 公开展示的通配符版本
*/
public static void reverse(List<?> list) {
reverseHelper(list); // 通配符捕获
}
/**
* 私有的泛型辅助方法 —— T 捕获 ?
*/
private static <T> void reverseHelper(List<T> list) {
int size = list.size();
for (int i = 0; i < size / 2; i++) {
T temp = list.get(i);
list.set(i, list.get(size - 1 - i));
list.set(size - 1 - i, temp);
}
}
public static void main(String[] args) {
List<String> names = new ArrayList<>(Arrays.asList("大翔", "白歌", "小崔"));
System.out.println("反转前:" + names);
reverse(names);
System.out.println("反转后:" + names);
List<Integer> nums = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
System.out.println("\n反转前:" + nums);
reverse(nums);
System.out.println("反转后:" + nums);
}
}
运行输出:
反转前:[大翔, 白歌, 小崔]
反转后:[小崔, 白歌, 大翔]
反转前:[1, 2, 3, 4, 5]
反转后:[5, 4, 3, 2, 1]
8. 通配符使用最佳实践
9. 易错场景
反例一:把 ? extends T 当容器往里装数据
// ❌ 错误:试图向 <? extends T> 集合添加元素
public void addTopStudent(List<? extends Student> students) {
Student top = findTop(students);
students.add(new Student("新生", 20, 0)); // ❌ 编译错误!
// 即使放回同一个对象也不行
students.add(top); // ❌ 编译错误!
}
纠正:
// ✅ 正确:用 ? super T 或精确类型
public void addTopStudent(List<Student> students) {
Student top = findTop(students);
students.add(new Student("新生", 20, 0)); // ✅ 精确类型——可读可写
}
// 或者只获取不修改
public Student findTop(List<? extends Student> students) {
return students.stream()
.max(Comparator.comparingDouble(Student::getScore))
.orElse(null);
}
反例二:混淆 ? extends 和 ? super 的读写方向
// ❌ 错误:从 <? super Student> 读取时以为是Student类型
public void printNames(List<? super Student> list) {
for (Student s : list) { // ❌ 编译错误!不能当作Student遍历
System.out.println(s.getName());
}
}
纠正:
// ✅ 正确:<? super Student> 只能以 Object 读取
public void printNames(List<? super Student> list) {
for (Object obj : list) { // ✅ 只能确保是 Object
if (obj instanceof Student) {
Student s = (Student) obj;
System.out.println(s.getName());
}
}
}
// 如果需要以 Student 类型读取,应该使用 ? extends Student 或精确类型
反例三:用通配符声明返回类型
// ❌ 不推荐:返回类型使用通配符 —— 调用方使用困难
public List<? extends Student> getStudents() {
List<CollegeStudent> list = new ArrayList<>();
// ...
return list;
}
// 调用方:
List<? extends Student> result = service.getStudents();
// result.add(new Student(...)); // ❌ 无法写入!调用方可能也需要写入
纠正:
// ✅ 正确:返回精确类型,参数使用通配符
public <T extends Student> List<T> getStudents(Class<T> type) {
List<T> list = new ArrayList<>();
// ...
return list;
}
// 调用方:
List<CollegeStudent> result = service.getStudents(CollegeStudent.class);
result.add(new CollegeStudent(...)); // ✅ 可以正常使用
10. 面试考点
Q1:List<? extends T> 和 List<? super T> 有什么区别?
List<? extends T>(上界通配符):只能安全地读取T类型数据,不能写入(除null)。用于生产者场景——从集合获取数据。List<? super T>(下界通配符):可以安全地写入T类型数据,读取只能到Object。用于消费者场景——向集合存放数据。
Q2:什么是PECS原则?
PECS = Producer Extends, Consumer Super。如果一个参数化类型是生产者(为你提供数据),用
<? extends T>;如果是消费者(接收你的数据),用<? super T>。这是Josh Bloch在《Effective Java》中提出的泛型使用核心原则。JDK中Collections.copy的签名就是教科书级的PECS应用。
Q3:List<?> 和 List(原始类型)有什么区别?
List<?>是泛型类型,编译器会进行类型安全检查——不能写入任何非null元素。List是原始类型,完全退出泛型检查——可以写入任何对象(仅警告),可以在类型间不安全转换。List<?>的安全性远高于原始类型。在需要"任意类型的List"时,始终使用List<?>而非原始类型。
Q4:为什么 List<Object> 不能接收 List<String> 参数?
因为Java泛型是不可协变的(invariant)。虽然
String是Object的子类,但List<String>不是List<Object>的子类型。如果允许这样做,就可以向List<String>中放入非String对象,破坏类型安全。解决方案是使用通配符:方法参数声明为List<? extends Object>(等价于List<?>)来接受任意类型的List。
Q5:通配符捕获是什么?如何使用?
通配符捕获是编译器的内部机制,将
List<?>中的?替换为一个内部捕获类型。当需要对List<?>执行"取出-放入"操作时(如交换两个元素),直接使用通配符会受到限制。解决方法是定义一个私有泛型辅助方法<T> void helper(List<T> list),让编译器通过方法调用完成捕获推断。