泛型通配符
通配符是什么?
? 是 Java 泛型里的「问号」,叫通配符。它代表「某种未知类型」,用来解决泛型类型系统的不灵活问题。
先看一个经典困惑:
java
List<Dog> dogs = new ArrayList<>();
List<Animal> animals = dogs; // 编译错误!List<Dog> 不是 List<Animal> 的子类——这叫不变性。
但实际开发中,我们确实需要「能放各种动物」的列表,怎么办?这时候通配符就登场了。
三种通配符
| 通配符 | 名称 | 含义 | 读写能力 |
|---|---|---|---|
? | 非限定通配符 | 未知类型 | 只能读,不能写 |
? extends T | 上界通配符 | T 或 T 的子类 | 只能读,不能写 |
? super T | 下界通配符 | T 或 T 的父类 | 可以写,读出来是 Object |
上界通配符 ? extends
「能读不能写」——适合生产者场景。
java
// 接收任何 Dog 或 Dog 的子类
public void printDogs(List<? extends Dog> dogs) {
for (Dog d : dogs) {
System.out.println(d); // 可以读,读出来是 Dog
}
dogs.add(new Dog()); // 编译错误!不能写
}
// 使用
List<Dog> myDogs = new ArrayList<>();
List<Husky> huskies = new ArrayList<>();
printDogs(myDogs); // OK
printDogs(huskies); // OK,因为 Husky extends Dog为什么不能写?因为编译器不知道具体是什么子类,可能是 Husky,也可能是 Chihuahua。
PECS 原则:Producer Extends
如果你的方法是产出数据的,用 extends:
java
// 生产者:读取数据
public double sum(List<? extends Number> numbers) {
double sum = 0;
for (Number n : numbers) { // 可以读
sum += n.doubleValue();
}
return sum;
}
List<Integer> ints = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);
System.out.println(sum(ints)); // OK
System.out.println(sum(doubles)); // OK下界通配符 ? super
「能写不能读(或读出来是 Object)」——适合消费者场景。
java
// 接收任何 Animal 或 Animal 的父类
public void addNumbers(List<? super Integer> list) {
list.add(1); // 可以写
list.add(2);
list.add(3);
// Object obj = list.get(0); // 可以读,但类型是 Object
}
// 使用
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
addNumbers(numbers); // OK,因为 Number 是 Integer 的父类
addNumbers(objects); // OK为什么能写?因为不管是 List<Integer>、List<Number> 还是 List<Object>,往里面加 Integer 都是安全的。
PECS 原则:Consumer Super
如果你的方法是消费数据的,用 super:
java
// 消费者:写入数据
public void copy(List<? super Integer> dest, List<? extends Integer> src) {
for (Integer i : src) { // 读出来是 Integer(生产者用 extends)
dest.add(i); // 写入 Integer(消费者用 super)
}
}非限定通配符 ?
「只能读,读出来是 Object」:
java
public void printAll(List<?> list) {
for (Object item : list) { // 只能读到 Object
System.out.println(item);
}
list.add("test"); // 编译错误!不能写
}什么时候用?当你只关心「有数据」而不关心「什么类型」时。
三种通配符对比
| 场景 | 通配符 | 能做什么 | 不能做什么 |
|---|---|---|---|
| 只读取数据 | ? extends T | 读 T | 写任何值 |
| 只写入数据 | ? super T | 写 T | 读出来不是 T |
| 既不读也不写 | ? | 只读 Object | 写任何值 |
典型应用场景
场景一:合并列表
把两个列表合并成一个:
java
// 错误的写法
public void wrongMerge(List<Number> dest, List<Number> src) {
dest.addAll(src); // src 传入 List<Integer> 会编译错误
}
// 正确的写法:用 extends 作为生产者
public <T> void merge(List<? super T> dest, List<? extends T> src) {
dest.addAll(src); // 完美
}
List<Number> numbers = new ArrayList<>();
List<Integer> ints = Arrays.asList(1, 2, 3);
merge(numbers, ints); // OK场景二:找最大值
java
public <T extends Comparable<? super T>> T max(List<T> list) {
if (list == null || list.isEmpty()) {
return null;
}
T max = list.get(0);
for (T item : list) {
if (item.compareTo(max) > 0) {
max = item;
}
}
return max;
}场景三:二叉树节点
java
public class TreeNode<T> {
T value;
TreeNode<? extends T> left;
TreeNode<? extends T> right;
}常见陷阱
陷阱一:误用通配符导致无法写入
java
List<? extends Number> numbers = new ArrayList<Integer>();
numbers.add(1); // 编译错误!因为 numbers 可能是 ArrayList<Double>,往 Double 列表里加 Integer 会出问题。
陷阱二:泛型方法 vs 通配符
java
// 泛型方法:可以同时支持读写
public static <T> void swap(List<T> list, int i, int j) {
T temp = list.get(i);
list.set(i, list.get(j));
list.set(j, temp);
}
// 通配符:只能读或只能写
// 无法用通配符实现 swap总结
记住 PECS 原则就够了:
- Producer Extends:如果你只从列表里读取数据,用
extends - Consumer Super:如果你只往列表里写入数据,用
super
既读又写?老老实实用泛型方法 <T>。
