教程中心 / Java教程 / 通配符

通配符

12分钟 泛型

什么是通配符?

通配符(Wildcard)是泛型中的一种特殊类型,它用?表示"未知类型"。与泛型类/方法的类型参数T不同,通配符主要用于方法参数,用来实现更灵活的赋值兼容性。

生活比喻

通配符就像一张"职位申请表"上的空白——不指定具体是谁,但知道是某类人。比如"招聘对象:计算机相关专业"(extends),或者"招收:能教书的"(super)。通配符就是这种模糊但有边界的描述。

三种通配符详解

通配符名称含义可读可写
?无界通配符未知类型✓(返回Object)
? extends T上界通配符T或T的子类✓(返回T)
? super T下界通配符T或T的父类✓(返回Object)✓(可写入T)

无界通配符(?)

?表示任意类型。当你只需要读取集合元素(当Object处理)而不需要写入时使用。

import java.util.List;
import java.util.ArrayList;

public class UnboundedWildcardDemo {
    public static void main(String[] args) {
        List ints = new ArrayList<>();
        ints.add(1);
        printList(ints);

        List strings = new ArrayList<>();
        strings.add("Hello");
        printList(strings);
    }

    // 使用无界通配符 - 可以接受任何类型的List
    public static void printList(List list) {
        for (Object item : list) {
            System.out.println("元素: " + item);
        }
        // 写入会编译错误:
        // list.add("test");  // 错误!
        // list.add(1);       // 错误!
    }
}

上界通配符(? extends T)

? extends T表示T或T的子类。你可以把T的子类型集合赋给它。由于不知道具体是哪个子类,写入会很危险,所以只读不写

理解"只读"

想象你有一个"水果盘"(Fruit[]),你只知道里面装的是水果(Fruit的子类),但不知道是苹果还是香蕉。你可以读取任何一个——但你不能往里面放东西,因为你不知道那个位置原来放的是什么类型的盘子。

import java.util.List;
import java.util.ArrayList;

public class UpperBoundWildcardDemo {
    public static void main(String[] args) {
        List ints = Arrays.asList(1, 2, 3, 4, 5);
        List doubles = Arrays.asList(1.1, 2.2, 3.3);

        System.out.println("Integer列表和: " + sumOfList(ints));
        System.out.println("Double列表和: " + sumOfList(doubles));
    }

    // 上界通配符 - 接受Number或Number子类的列表
    public static double sumOfList(List list) {
        double sum = 0;
        for (Number n : list) {
            sum += n.doubleValue();  // 可以安全读取为Number
        }
        // 写入会编译错误:
        // list.add(Integer.valueOf(1));  // 错误! 可能是Double列表
        return sum;
    }
}

下界通配符(? super T)

? super T表示T或T的父类。这时你可以安全地写入T及其子类,因为无论如何,写入的元素都会被当作T或T的父类。但读取时只能当Object处理。

理解"可写"

想象你有一个"容器",只要它能装"数字"(Number)就行。你往里面放Integer、Double都可以,因为它们都是Number。但如果容器声明的是Object,你就不能保证取出来的是特定类型——只能当Object处理。

import java.util.List;
import java.util.ArrayList;

public class LowerBoundWildcardDemo {
    public static void main(String[] args) {
        List numbers = new ArrayList<>();
        List objects = new ArrayList<>();

        addNumbers(numbers);  // OK - Number是Integer的父类
        addNumbers(objects);   // OK - Object是Number的父类

        System.out.println("Number列表: " + numbers);
        System.out.println("Object列表: " + objects);
    }

    // 下界通配符 - 接受Integer或Integer父类的列表
    public static void addNumbers(List list) {
        // 可以安全写入Integer
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);

        // 读取只能是Object
        Object obj = list.get(0);
        System.out.println("读取第一个: " + obj);

        // 以下写入会编译错误:
        // list.add("hello");  // 错误! String不是Integer的子类
    }
}

                        

PECS原则

PECS(Producer-Extends, Consumer-Super)是记住何时用extends何时用super的最简单方法:

PECS原则详解:

    ┌────────────────────────────────────────────────────────────┐
    │                                                            │
    │   如果你的泛型变量是"生产者"(往外提供数据)→ 用 extends   │
    │   如果你的泛型变量是"消费者"(往里写入数据)→ 用 super     │
    │                                                            │
    │   记忆口诀:"生产者用extends,消费者用super"               │
    │            "Extends提供数据,Super消费数据"                │
    └────────────────────────────────────────────────────────────┘

PECS示例:完整的鸳鸯锅

import java.util.List;
import java.util.ArrayList;

public class PECSDemo {
    public static void main(String[] args) {
        // 生产者场景 - 需要往外读取数据 → 用 extends
        List ints = Arrays.asList(1, 2, 3, 4, 5);
        List doubles = Arrays.asList(1.1, 2.2, 3.3);

        System.out.println("Integer最大值: " + max(ints));
        System.out.println("Double最大值: " + max(doubles));

        // 消费者场景 - 需要往里写入数据 → 用 super
        List numbers = new ArrayList<>();
        List objects = new ArrayList<>();

        copy(ints, numbers);   // Integer列表复制到Number列表
        copy(doubles, objects); // Double列表复制到Object列表

        System.out.println("Numbers: " + numbers);
        System.out.println("Objects: " + objects);
    }

    // 生产者 - 需要读取元素 → 用 extends
    public static double max(List list) {
        if (list == null || list.isEmpty()) {
            return 0;
        }
        double max = list.get(0).doubleValue();
        for (Number n : list) {
            if (n.doubleValue() > max) {
                max = n.doubleValue();
            }
        }
        return max;
    }

    // 消费者 - 需要写入元素 → 用 super
    public static  void copy(List source, List dest) {
        for (T item : source) {
            dest.add(item);  // 安全写入
        }
    }
}

                        

什么时候用通配符而不是泛型方法?

场景推荐使用原因
只需要读? extends T灵活接受T及其子类
只需要写? super T灵活接受T及其父类
既读又写<T>泛型方法两边都需要确定类型
只做Object操作?不需要知道具体类型

常见错误

错误1:同时使用extends和super

List - 这不是同时用,而是多重限定(一个上界同时实现多个接口),是合法的。

错误2:在泛型变量中混用读写

如果既想读又想写,不要用通配符,用泛型方法:public static void copy(List source, List dest)

小结

  • ? extends T:读取T及其子类,写入会危险(只读)
  • ? super T:写入T及其父类,读取只能当Object(可写)
  • ?(无界):只读Object,操作最受限
  • PECS原则:生产者用extends,消费者用super
  • 泛型方法适合既读又写的场景