Skip to content

泛型通配符

通配符是什么?

? 是 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>

基于 VitePress 构建