首页 / Java 入门教程 / 泛型通配符与边界

Java 入门教程

泛型通配符与边界

本教程共 100 篇 · 第 84 篇 · 更新于 2026-08-05 · 约 5 分钟阅读

JavaJava 入门教程泛型通配符PECS边界

本节目标:搞懂泛型通配符 ? 的三种用法(无界、上界 extends、下界 super),记住 PECS 口诀,学完知道什么时候该用哪种边界、为什么 extends 不能存、super 不能取。

为什么需要通配符

有时候你并不关心具体是哪种类型,只想写个「能处理任意 List」的方法。比如打印任意列表:

static void printList(List<?> list) { // ? 表示任意类型
    for (Object o : list) {
        System.out.println(o);
    }
}

List<?> 里的 ? 就是通配符,意思是「某种未知类型」。它比 List<Object> 宽松得多——List<String>List<Integer> 都能传进 printList,而 List<Object> 反而接不住 List<String>(泛型不协变)。

Note

List<Object> 不能接收 List<String>,这是泛型为了类型安全刻意的设计。想接收「各种 List」,必须用通配符 List<?>,而不是 List<Object>

上界 ? extends:生产者,只能取

? extends T 表示「T 或 T 的子类」。它常出现在「你只从里面取数据」的场景。

static double sum(List<? extends Number> list) {
    double s = 0;
    for (Number n : list) {
        s += n.doubleValue(); // 取出来至少是 Number,安全
    }
    return s;
}

为什么叫「只能取」?因为编译器只知道里面是「某个 Number 子类」,但不知道具体是哪个。你往里 add(整数) 它可能实际是个 List<Double>,加整数就破坏了类型安全,所以编译器直接禁止你 add(除了 null)。

Warning

List<? extends Number> 不能调用 add(除了 null)。记住:上界通配符适合「读」,不适合「写」。

下界 ? super:消费者,只能存

? super T 表示「T 或 T 的父类」。它常出现在「你只往里面写数据」的场景。

static void addNumbers(List<? super Integer> list) {
    list.add(1);   // 安全:Integer 一定能放进「Integer 或其父类」的列表
    list.add(2);
}

这里编译器知道列表的元素类型至少是 Integer 的父类,所以往里加 Integer 一定安全。但取出来的元素类型不确定(可能是 Object),所以往外取时只能当 Object 用。

Note

下界通配符适合「写」,不适合「读」。取出来的元素类型被擦成父类(甚至 Object),拿不到具体子类的丰富方法。

PECS 口诀

上面这套规律有个经典口诀:PECS = Producer Extends, Consumer Super

  • 如果参数「生产」数据给你(你从中读取)→ 用 ? extends T
  • 如果参数「消费」数据(你往里写入)→ 用 ? super T

Java 标准库里 Collections.copy 就是典范:

// dest 是消费者(往里写),src 是生产者(从中读)
static <T> void copy(List<? super T> dest, List<? extends T> src) { ... }

记住 PECS,下次写泛型方法纠结边界时,先问自己:这个参数是被读还是被写?答案一出来,extends 还是 super 自然就定了。

无界通配符 ? 的常见用法

? 单独用,表示「任意类型」,常见于只做不依赖元素类型操作的场合,比如清空、判断是否为空:

static void clearAll(List<?> list) {
    // list.clear(); // 清空是允许的,不关心元素类型
}

注意 List<?>List<? extends Object> 基本等价,都用「读出来是 Object、不能 add」的语义。

通配符不能当自己的类型参数

有个常见误解:能不能在自己写的泛型类里用 ? 当参数?不能。? 只在「使用泛型」时表示未知,定义泛型类/方法时要用具名参数如 <T>

class Box<?> { }     // 错误!定义处不能用 ?
class Box<T> { }     // 正确,用具名类型参数
Warning

通配符 ? 只能出现在「调用/声明变量」的位置(如 List<?>void f(List<? extends T>)),不能出现在「定义泛型类或泛型方法」的类型参数声明处。那地方要用 TE 这种名字。

通配符捕获

如果你拿到一个 List<?>,想往里加元素又受限于「不能 add」,可以用泛型方法把 ? 「捕获」成具名 T 来绕过:

static <T> void swapFirst(List<T> list) { /* 现在能安全操作 T 了 */ }
// 调用时即使传 List<?> 也能匹配,因为 ? 被捕获成某个具体 T

这属于进阶技巧,知道有这条路即可,日常写业务很少需要它。

通配符和数组的对比

数组是协变的:Integer[] 可以赋给 Number[],但运行时才检查类型,容易 ArrayStoreException。泛型反之,故意「不协变」来保证编译期安全,所以才需要通配符来在不破坏安全的前提下获得灵活性。理解这点,就懂了为什么泛型设计得「别扭」——它是用别扭换安全,值。

实践中的选择清单

把前面讲的收敛成一张速查表,写泛型方法拿不准边界时照着对:

  • 只读取、不写入 → ? extends T(生产者)
  • 只写入、不读取 → ? super T(消费者)
  • 既不读也不写(如清空、计数)→ ? 无界
  • 既要读又要写、且类型确定 → 用具名 <T>,而不是通配符

PECS 不是死规矩

PECS 是经验法则,帮你在 90% 场景快速决策。极端复杂时可能需要折中,但先养成「先想读写方向」的习惯,代码正确率会明显提升。泛型通配符是 Java 里偏难的一块,多写几次、多对着速查表练,自然就顺了。

一个常见陷阱提醒

List<? extends Number> 看似能 add 任何 Number 子类,实则连一个具体元素都加不进去(编译器不知道实际类型)。如果你发现自己「又想读又想写」还用了 extends,多半是边界选错了——这种场景应该用具名 <T> 参数,而不是通配符。

一句话收尾

通配符这套规则初看绕,核心就一句:先想清楚这个参数是「被读」还是「被写」,再决定 extends 还是 super。PECS 是帮你快速拍板的经验法则,多写几次就形成肌肉记忆了。泛型通配符是 Java 里偏难的一块,但也是体现类型系统设计功力的地方,啃下来很值。

小结

通配符 ? 让泛型方法能接收「各种具体类型」。上界 ? extends T 适合读(生产者),下界 ? super T 适合写(消费者),合起来就是 PECS 口诀。通配符只能在使用处出现,定义泛型类/方法要用具名参数。到此,集合与泛型两大家族的基础就讲完了。