泛型通配符
概述
在掌握 泛型基础 后,我们常会碰到这样的需求:方法参数要能接受「某种泛型类型」的多种具体化,例如既能接受 List<String> 又能接受 List<Integer>,同时又希望保持类型安全。通配符(wildcard)? 以及上界 ? extends X、下界 ? super X 就是用来表达这种「未知但有限制」的类型,让 API 更灵活且仍能在编译期做检查。
通俗理解:通配符表示「这里可以是某种类型,但我不在方法签名里写死具体类型」,由调用方传入的实参决定;extends 和 super 则给这个「某种类型」划出边界,从而决定在方法体内能读还是写该泛型结构。
前置建议
已掌握 泛型基础(泛型类、泛型方法、类型擦除)。通配符在集合 API(如 List、Collections 工具类)中大量出现,学完后阅读其方法签名会更容易理解。
为什么需要通配符
若只用「确定类型」的泛型,会出现不够灵活的情况。例如:希望写一个打印任意 List 元素的方法。
仅用泛型方法时的局限
// 只能接受 List<String>,不能接受 List<Integer> 等
public static void printList(List<String> list) {
for (String s : list) {
System.out.println(s);
}
}若改成 List<Object>,又会失去类型安全,且 List<Integer> 并不是 List<Object> 的子类型(泛型是不可协变的):
List<Integer> intList = Arrays.asList(1, 2, 3);
// 编译错误:List<Integer> 不能赋给 List<Object>
// printList(intList); // 假设方法签名是 List<Object>在 Java 中,List<Integer> 与 List<String> 都不是 List<Object> 的子类型,因此不能用一个「具体类型」的 List<X> 统一表示「元素类型任意的 List」。这时就需要通配符:用 List<?> 表示「元素类型未知的 List」,在方法体内按「只读」方式使用,既安全又灵活。
无界通配符 ?
无界通配符写作 ?,表示「某种类型,但类型本身未知」。常用于:只关心「是一个泛型结构」,而不关心其类型参数是什么。
基本用法
// 接受任意元素类型的 List,只做读取
public static void printList(List<?> list) {
for (Object elem : list) {
System.out.println(elem);
}
}
// 调用
printList(Arrays.asList("a", "b")); // List<String>
printList(Arrays.asList(1, 2, 3)); // List<Integer>在方法体内,从 List<?> 中取出的元素只能视为 Object(因为类型未知),因此只能读取、不能写入(除了写入 null):
public static void demo(List<?> list) {
Object o = list.get(0); // 可以:读出的用 Object 接
// list.add("hello"); // 编译错误:不能向 List<?> 写入除 null 外的值
list.add(null); // 可以:null 属于所有引用类型
}提示
List<?> 与 List<Object> 不同:前者表示「元素类型未知的 List」,可接受 List<String>、List<Integer> 等;后者表示「元素类型是 Object 的 List」,只能接受 List<Object>,不能接受 List<String>。
适用场景
- 方法只读取泛型结构中的元素,且不依赖具体类型时,用
List<?>、Set<?>等即可。 - 不关心类型参数时,用
?比强行指定Object更准确,也更能表达「只读」语义。
上界通配符 ? extends X
上界通配符写作 ? extends 类型,表示「未知类型,但它是给定类型或其子类型」。这样在方法体内可以安全地把读出的值当作上界类型使用(例如 Number),因此适合只读、生产场景。
基本用法
// 接受 List<Number>、List<Integer>、List<Double> 等
public static double sum(List<? extends Number> list) {
double total = 0;
for (Number n : list) {
total += n.doubleValue();
}
return total;
}
// 调用
sum(Arrays.asList(1, 2, 3)); // List<Integer>
sum(Arrays.asList(1.5, 2.5)); // List<Double>
sum(Arrays.asList(1, 2.5, 3)); // List<Number>因为 ? 代表的是 Number 或其子类型,所以从列表中取出的元素至少是 Number,可以安全调用 doubleValue() 等方法。
只读语义
使用 ? extends X 时,编译器只知道「元素是 X 或其子类型」,但不知道具体是哪个子类型,因此不能向该 List 写入除 null 外的任何值(否则可能破坏调用方传入的 List<Integer> 等类型约束):
public static void addOne(List<? extends Number> list) {
// list.add(1); // 编译错误:不能写入
// list.add(1.0); // 编译错误:不能写入
list.add(null); // 可以
}注意
? extends Number 表示「某种 Number 子类型」,可能是 Integer、Double 等。若允许 list.add(1),则当实参是 List<Double> 时就会混入 Integer,破坏类型安全,因此编译器禁止写入。
示例:结合泛型方法
// 从「元素类型为 Number 或其子类型」的列表中取第一个元素,返回 Number
public static Number getFirst(List<? extends Number> list) {
return list.isEmpty() ? null : list.get(0);
}下界通配符 ? super X
下界通配符写作 ? super 类型,表示「未知类型,但它是给定类型或其父类型」。这样在方法体内可以安全地向该泛型结构写入该类型(例如 Integer),因此适合只写、消费场景。
基本用法
// 向 list 中放入 Integer(或 Integer 的子类型),list 的元素类型可以是 Integer、Number 或 Object
public static void addIntegers(List<? super Integer> list) {
list.add(1);
list.add(2);
}
// 调用
List<Integer> intList = new ArrayList<>();
addIntegers(intList);
List<Number> numList = new ArrayList<>();
addIntegers(numList);
List<Object> objList = new ArrayList<>();
addIntegers(objList);因为 ? 代表的是 Integer 或其父类型,所以往其中放入 Integer 是安全的;但从该 List 中读出时只能得到 Object(因为不知道实际是 Integer、Number 还是 Object)。
只写语义(读取为 Object)
public static void copyTo(List<? super Integer> dest, List<Integer> src) {
for (Integer i : src) {
dest.add(i); // 可以:Integer 可放入 ? super Integer
}
// 从 dest 取出时只能按 Object 使用
Object o = dest.get(0);
}PECS 原则
PECS 即 Producer extends, Consumer super,是使用通配符时的实用口诀:
| 角色 | 通配符 | 含义 |
|---|---|---|
| Producer(生产者) | ? extends T | 结构会产出类型为 T(或其子类型)的元素,只读 |
| Consumer(消费者) | ? super T | 结构会消费类型为 T(或其子类型)的元素,可写 |
- 若参数是数据的来源(只从里读),用
? extends T。 - 若参数是数据的目标(只往里写),用
? super T。 - 若既要读又要写,且读写类型一致,则不用通配符,直接用具体类型如
List<T>。
示例:Collections.copy
标准库中的 Collections.copy 就是一个典型用法:
// 源码签名(简化)
public static <T> void copy(List<? super T> dest, List<? extends T> src)src是生产者:从中读取T类型元素,故用List<? extends T>。dest是消费者:向其中写入T类型元素,故用List<? super T>。
List<Number> dest = new ArrayList<>();
List<Integer> src = Arrays.asList(1, 2, 3);
Collections.copy(dest, src); // 正确:Integer 可写入 Number 的 List对比小结
| 通配符 | 可读性 | 可写性 | 典型用途 |
|---|---|---|---|
? | 读出为 Object | 仅可写 null | 完全未知类型、只读 |
? extends T | 读出为 T | 仅可写 null | 生产者、只读 |
? super T | 读出为 Object | 可写 T 及其子类型 | 消费者、只写 |
注意事项
注意
- 通配符不能出现在
new表达式中:不能写new ArrayList<?>(),只能写new ArrayList<>()等具体类型或裸类型。 - 同一方法中,若某个类型既是生产者又是消费者(既要读又要写且类型一致),应使用泛型方法形如
<T> void method(List<T> list),而不是List<?>。
提示
在声明方法参数时,优先考虑「只读用 extends、只写用 super」,再结合 PECS 检查一遍,能减少编译错误并提高 API 的通用性。在 集合 与 Collections 工具类 的很多方法中都会看到这种写法。
相关链接
- 泛型基础 — 泛型类、泛型方法、类型擦除
- 集合概述 — 集合框架中的泛型与通配符用法
- List、Set、Map 详解 — 常见集合的读写与遍历
- Oracle Java 教程 - 泛型通配符