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 泛型,提高代码的灵活性和可维护性。🚀