139. Java 泛型 - Java 通配符使用准则
在 Java 泛型编程中,通配符(?)的使用是一个让许多开发者感到困惑的话题。特别是在决定何时使用**上限通配符(? extends T)和 下限通配符(? super T)**时,更需要遵循一些准则来确保代码的可读性和安全性。
1. "in" 和 "out" 原则
在确定是否使用通配符以及选择适当的通配符时,可以采用 "in" 和 "out" 原则 ,这也是**PECS**(Producer Extends, Consumer Super)的核心思想:
- "in" 变量(输入变量) :
- 提供数据 供方法使用(
Producer)。 - 使用
extends关键字(上限通配符)。 - 例如:
List<? extends Number>表示这个列表只能读取Number或其子类的元素,但不能添加新的元素(除null外)。
- 提供数据 供方法使用(
- "out" 变量(输出变量) :
- 存储数据 以供方法输出(
Consumer)。 - 使用
super关键字(下限通配符)。 - 例如:
List<? super Integer>表示这个列表可以存储Integer或其超类的元素,但读取时只能作为Object处理。
- 存储数据 以供方法输出(
- 既是 "in" 又是 "out" 的变量 :
- 不能使用通配符,应使用明确的泛型类型。
- 例如:
List<T>,如果方法既要读取 又要写入 元素,使用具体的类型T而不是通配符。
2. 何时使用 extends(上限通配符)
当一个变量仅作为输入数据 (Producer)时,应使用 ? extends T,它表示该变量是 T 或 T 的子类型。
示例 1:使用 extends 处理只读数据
java
import java.util.Arrays;
import java.util.List;
public class UpperBoundExample {
public static double sum(List<? extends Number> numbers) {
double sum = 0;
for (Number num : numbers) {
sum += num.doubleValue();
}
return sum;
}
public static void main(String[] args) {
List<Integer> intList = Arrays.asList(1, 2, 3);
List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);
System.out.println(sum(intList)); // ✅ 输出:6.0
System.out.println(sum(doubleList)); // ✅ 输出:6.6
}
}
解析
List<? extends Number>允许Integer、Double等Number子类的列表作为参数传递。- 但是,不能向
numbers列表添加元素 (除了null),因为编译器无法确定它具体是什么类型。
示例 2:为什么不能向 ? extends T 添加元素?
java
List<? extends Number> numList = new ArrayList<>();
numList.add(5); // ❌ 编译错误
原因
? extends Number表示该列表可能是List<Integer>、List<Double>等,但 Java 无法确定具体类型。- 因此,不能向
numList添加元素(除了null),以避免潜在的类型错误。
3. 何时使用 super(下限通配符)
当一个变量仅用于存储数据 (Consumer)时,应使用 ? super T,它表示该变量是 T 或 T 的超类。
示例 3:使用 super 处理可写入数据
java
import java.util.List;
import java.util.ArrayList;
public class LowerBoundExample {
public static void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
public static void main(String[] args) {
List<Number> numList = new ArrayList<>();
addNumbers(numList);
System.out.println(numList); // ✅ 输出:[1, 2, 3]
List<Object> objList = new ArrayList<>();
addNumbers(objList);
System.out.println(objList); // ✅ 输出:[1, 2, 3]
}
}
解析
List<? super Integer>允许Integer、Number、Object类型的列表作为参数传递。- 这样,方法可以安全地向列表添加
Integer类型的元素 ,因为Integer是T或T的子类型。 - 但是,读取时只能作为
Object处理,因为编译器无法确定它具体存储的是什么类型。
示例 4:为什么不能从 ? super T 读取具体类型?
java
List<? super Integer> list = new ArrayList<>();
list.add(42); // ✅ 可以添加 Integer
Integer num = list.get(0); // ❌ 编译错误
原因
? super Integer可能是List<Integer>、List<Number>或List<Object>。- 读取
list.get(0)时,编译器只能保证它是Object,但不能确定它是Integer,所以不能直接赋值给Integer。
4. 何时使用无界通配符 ?
如果变量可以使用 Object 的方法(如 toString()、equals()),但不涉及泛型特定的操作,使用无界通配符 ? 更合适。
示例 5:无界通配符适用于打印数据
java
import java.util.List;
public class UnboundedExample {
public static void printList(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
public static void main(String[] args) {
List<String> strList = List.of("A", "B", "C");
List<Integer> intList = List.of(1, 2, 3);
printList(strList); // ✅ 输出 A B C
printList(intList); // ✅ 输出 1 2 3
}
}
解析
List<?>允许任何类型 的List作为参数。- 但只能将元素当作
Object读取,不能进行写操作(除了null)。
5. 避免在返回类型中使用通配符
应避免在方法的返回类型中使用通配符,否则调用者无法确定返回值的确切类型。
错误示例
java
public static List<? extends Number> getNumbers() {
return new ArrayList<Integer>();
}
问题
- 方法的返回类型是
List<? extends Number>,但调用者不知道它的具体类型,无法向列表添加元素。 - 可能导致不必要的类型转换错误。
更好的做法
java
public static List<Number> getNumbers() {
return new ArrayList<>();
}
这样,返回类型更清晰,调用者可以正确操作列表中的元素。
6. 结论
| 情况 | 关键字 | 作用 |
|---|---|---|
| 仅读取数据 | ? extends T |
上限通配符,适用于"Producer"(提供数据) |
| 仅写入数据 | ? super T |
下限通配符,适用于"Consumer"(消费数据) |
仅使用 Object 方法 |
? |
适用于泛型方法中不关心类型的情况 |
| 同时读取和写入 | 具体类型 T |
避免使用通配符 |
掌握这些规则,可以更安全、更高效地使用 Java 泛型,提高代码的灵活性和可维护性。🚀