Java 入门教程
泛型通配符与边界
本教程共 100 篇 · 第 84 篇 · 更新于 2026-08-05 · 约 5 分钟阅读
JavaJava 入门教程泛型通配符PECS边界
84. 泛型通配符与边界
本节目标:搞懂泛型通配符
?的三种用法(无界、上界 extends、下界 super),记住 PECS 口诀,学完知道什么时候该用哪种边界、为什么 extends 不能存、super 不能取。
为什么需要通配符
有时候你并不关心具体是哪种类型,只想写个「能处理任意 List」的方法。比如打印任意列表:
static void printList(List<?> list) { // ? 表示任意类型
for (Object o : list) {
System.out.println(o);
}
}
```bash
`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 的子类」。它常出现在「你只从里面取数据」的场景。
```java
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);
}
```bash
这里编译器知道列表的元素类型至少是 `Integer` 的父类,所以往里加 `Integer` 一定安全。但取出来的元素类型不确定(可能是 Object),所以往外取时只能当 `Object` 用。
> [!NOTE]
> 下界通配符适合「写」,不适合「读」。取出来的元素类型被擦成父类(甚至 Object),拿不到具体子类的丰富方法。
## PECS 口诀
上面这套规律有个经典口诀:**PECS = Producer Extends, Consumer Super**。
- 如果参数「生产」数据给你(你从中读取)→ 用 `? extends T`。
- 如果参数「消费」数据(你往里写入)→ 用 `? super T`。
Java 标准库里 `Collections.copy` 就是典范:
```java
// dest 是消费者(往里写),src 是生产者(从中读)
static <T> void copy(List<? super T> dest, List<? extends T> src) { ... }
记住 PECS,下次写泛型方法纠结边界时,先问自己:这个参数是被读还是被写?答案一出来,extends 还是 super 自然就定了。
无界通配符 ? 的常见用法
? 单独用,表示「任意类型」,常见于只做不依赖元素类型操作的场合,比如清空、判断是否为空:
static void clearAll(List<?> list) {
// list.clear(); // 清空是允许的,不关心元素类型
}
```bash
注意 `List<?>` 和 `List<? extends Object>` 基本等价,都用「读出来是 Object、不能 add」的语义。
## 通配符不能当自己的类型参数
有个常见误解:能不能在自己写的泛型类里用 `?` 当参数?不能。`?` 只在「使用泛型」时表示未知,定义泛型类/方法时要用具名参数如 `<T>`。
```java
class Box<?> { } // 错误!定义处不能用 ?
class Box<T> { } // 正确,用具名类型参数
Warning通配符
?只能出现在「调用/声明变量」的位置(如List<?>、void f(List<? extends T>)),不能出现在「定义泛型类或泛型方法」的类型参数声明处。那地方要用T、E这种名字。
通配符捕获
如果你拿到一个 List<?>,想往里加元素又受限于「不能 add」,可以用泛型方法把 ? 「捕获」成具名 T 来绕过:
static <T> void swapFirst(List<T> list) { /* 现在能安全操作 T 了 */ }
// 调用时即使传 List<?> 也能匹配,因为 ? 被捕获成某个具体 T
```bash
这属于进阶技巧,知道有这条路即可,日常写业务很少需要它。
## 通配符和数组的对比
数组是协变的:`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 口诀。通配符只能在使用处出现,定义泛型类/方法要用具名参数。到此,集合与泛型两大家族的基础就讲完了。