-
-
- 泛型通配符是什么?
- 三种基本通配符
-
- [1. 无边界通配符:`<?>`](#1. 无边界通配符:
<?>) - [2. 上界通配符:`<? extends T>`](#2. 上界通配符:
<? extends T>) - [3. 下界通配符:`<? super T>`](#3. 下界通配符:
<? super T>)
- [1. 无边界通配符:`<?>`](#1. 无边界通配符:
- [PECS原则:Producer Extends, Consumer Super](#PECS原则:Producer Extends, Consumer Super)
- 实际应用举例
-
- [1. 方法参数设计](#1. 方法参数设计)
- [2. 集合操作工具方法](#2. 集合操作工具方法)
- 常见误区和注意事项
-
- [1. 通配符不能用于创建对象](#1. 通配符不能用于创建对象)
- [2. 通配符不能作为泛型方法的实际类型参数](#2. 通配符不能作为泛型方法的实际类型参数)
- [3. 理解边界的方向](#3. 理解边界的方向)
- 总结
-
泛型通配符是什么?
在学习Java泛型的过程中,我们经常会遇到三个特殊的符号:?、extends 和 super。它们组合起来形成的通配符(wildcard)是泛型中最让人头疼的部分,但也是最强大的特性之一。
简单来说,通配符就是用来表示"某种不确定的类型"。就像我们在填表时遇到的"其他"选项一样,当具体类型不重要或者无法确定时,就可以用通配符来代替。
三种基本通配符
1. 无边界通配符:<?>
这是最简单的通配符形式。<?> 表示"某个未知的类型"。
java
List<?> list = new ArrayList<String>(); // 可以指向任何类型的List
list = new ArrayList<Integer>(); // 也可以重新赋值为其他类型
使用 <?> 的限制是:你不能向其中添加除了 null 以外的任何对象,因为你不知道实际的类型是什么。
java
List<?> list = new ArrayList<String>();
list.add(null); // 这个可以
// list.add("hello"); // 编译错误!
2. 上界通配符:<? extends T>
这种写法表示"某个继承自 T 的类型",或者说"某种 T 类型或其子类型"。
java
List<? extends Number> numbers = new ArrayList<Integer>(); // Integer是Number的子类
numbers = new ArrayList<Double>(); // Double也是Number的子类
为什么要叫"上界"?因为 [Number]是上限,实际类型必须是 [Number]或其子类。
使用上界通配符时的限制:
- 可以安全地从中读取数据(因为读到的肯定能当成 [Number]处理)
- 不能向其中写入数据(因为不知道具体是哪种子类型)
java
List<? extends Number> numbers = new ArrayList<Integer>();
Number num = numbers.get(0); // 可以读取,肯定是Number类型
// numbers.add(new Integer(1)); // 编译错误!
3. 下界通配符:<? super T>
这种写法表示"某个 T 的父类型",也就是"某种 T 类型或其父类型"。
java
List<? super Integer> integers = new ArrayList<Number>(); // Number是Integer的父类
integers = new ArrayList<Object>(); // Object是所有类的父类
这里 Integer 是下限,实际类型必须是 Integer 或其父类。
使用下界通配符时的特点:
- 可以安全地向其中写入
T类型的数据 - 从中读取 数据时只能当作
Object处理(因为不知道具体是哪个父类)
java
List<? super Integer> integers = new ArrayList<Number>();
integers.add(new Integer(1)); // 可以写入Integer
integers.add(42); // 自动装箱也行
// Integer num = integers.get(0); // 错误!只能当Object用
Object obj = integers.get(0); // 正确!
PECS原则:Producer Extends, Consumer Super
这是使用泛型通配符的重要口诀:
- Producer Extends :如果你只是从容器中获取数据(生产者),使用
<? extends T> - Consumer Super :如果你只是向容器中放入数据(消费者),使用
<? super T>
让我们通过一个具体的例子来理解:
java
public static void copy(List<? extends Number> source,
List<? super Number> destination) {
for (Number num : source) { // source是生产者,用extends
destination.add(num); // destination是消费者,用super
}
}
在这个例子中:
-
source\]列表我们只从中读取数据,所以用 ` extends Number>`
实际应用举例
1. 方法参数设计
java
// 错误的设计:太严格了
public static void printList(List<Object> list) {
for (Object item : list) {
System.out.println(item);
}
}
// 正确的设计:灵活得多
public static void printList(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
// 使用
List<String> strings = Arrays.asList("a", "b", "c");
List<Integer> integers = Arrays.asList(1, 2, 3);
printList(strings); // 现在可以用了
printList(integers); // 这个也可以
2. 集合操作工具方法
java
// 向一个列表中添加另一个列表的所有元素
public static <T> void addAll(List<? super T> dest, List<? extends T> src) {
for (T item : src) {
dest.add(item);
}
}
// 使用
List<Object> objects = new ArrayList<>();
List<String> strings = Arrays.asList("hello", "world");
addAll(objects, strings); // 可以把String列表加到Object列表中
常见误区和注意事项
1. 通配符不能用于创建对象
java
// 错误!
// List<?> list = new ArrayList<?>();
// 正确的做法
List<?> list = new ArrayList<String>();
2. 通配符不能作为泛型方法的实际类型参数
java
public static <T> void method(List<T> list) {
// ...
}
// 错误!
// method(new ArrayList<?>());
// 正确的做法
List<String> stringList = new ArrayList<>();
method(stringList);
3. 理解边界的方向
很多人容易搞混 extends 和 super 的方向:
java
// extends: 设置上界(上限)
List<? extends Number> // 实际类型是Number或其子类
// super: 设置下界(下限)
List<? super Number> // 实际类型是Number或其父类
总结
泛型通配符的核心思想是为了增加代码的灵活性:
<?>:完全不知道类型,只能读取为Object,基本不能写入<? extends T>:知道是T或其子类,可以安全读取为T,不能写入<? super T>:知道是T或其父类,可以安全写入T,读取只能当Object
记住PECS原则,在设计API时合理使用通配符,可以让代码既安全又灵活。刚开始接触时会觉得复杂,但多练习几次就很容易掌握了。