1. 引入通配符的动机
泛型虽然提供了类型安全,但在某些场景下显得过于严格,尤其是在处理继承关系时。
问题: 假设 Integer
是 Number
的子类。那么 List<Integer>
是 List<Number>
的子类吗? 答案是否定的!
泛型类型之间默认是不存在继承关系的,即使它们的类型参数之间有继承关系。这意味着你不能将一个 List<Integer>
赋值给一个 List<Number>
变量,也不能将 List<Integer>
作为参数传递给需要 List<Number>
的方法。
ini
List<Integer> integers = new ArrayList<>();
// List<Number> numbers = integers; // 编译错误! Incompatible types.
这样做是为了防止类型安全问题。如果允许 List<Number> numbers = integers;
,那么就可以通过 numbers.add(Double.valueOf(3.14));
往 integers
列表里添加 Double
类型,这显然破坏了 integers
列表只能包含 Integer
的约定。
但是,我们确实有需要处理"某一种 Number
的子类"的列表,或者"某一种 Integer
的父类"的列表的需求。这时就需要通配符。
2.通配符的概念
通配符 ?
用于表示未知的类型 。它使得泛型能够处理更灵活的子类型关系。通配符主要用在方法参数、字段类型或局部变量类型上,不能用于定义泛型类或泛型方法的类型参数(即 class MyClass<?>
是非法的)。
上界通配符:? extends Type
scss
public static double sumOfList(List<? extends Number> list) {
double sum = 0.0;
for (Number n : list) { // 可以安全地读取 Number
sum += n.doubleValue();
}
// list.add(Integer.valueOf(1)); // 编译错误! 不能添加
return sum;
}
// 使用
List<Integer> li = List.of(1, 2, 3);
List<Double> ld = List.of(1.1, 2.2, 3.3);
System.out.println("Sum of integers = " + sumOfList(li));
System.out.println("Sum of doubles = " + sumOfList(ld));
下界通配符:? super Type
scss
public static void addNumbers(List<? super Integer> list) {
list.add(1); // 可以安全地添加 Integer
list.add(2);
// Number n = list.get(0); // 编译错误! 读取出来的只能保证是 Object
Object obj = list.get(0); // 可以读取 Object
System.out.println("Added numbers to list. First element as Object: " + obj);
}
// 使用
List<Number> ln = new ArrayList<>();
List<Object> lo = new ArrayList<>();
List<Integer> li = new ArrayList<>();
addNumbers(ln); // Number 是 Integer 的父类
addNumbers(lo); // Object 是 Integer 的父类
addNumbers(li); // Integer 本身
System.out.println("List<Number>: " + ln);
System.out.println("List<Object>: " + lo);
System.out.println("List<Integer>: " + li);
无界通配符:?
ini
public static void printListInfo(List<?> list) {
System.out.println("List size: " + list.size());
// list.add("anything"); // 编译错误!
if (!list.isEmpty()) {
Object first = list.get(0); // 可以读取 Object
System.out.println("First element (as Object): " + first);
}
}
// 使用
List<String> ls = List.of("A", "B");
List<Integer> li = List.of(10, 20);
printListInfo(ls);
printListInfo(li);
3.泛型与通配符的关系与区别
- 泛型(如
<T>
) 是用来定义可以接受类型参数的类、接口或方法。它声明了一个类型变量。 - 通配符(如
?
,? extends T
,? super T
) 是用来使用泛型类型时,表示对类型参数的某种约束(未知类型、类型的上界或下界)。它本身不是类型变量,而是表示某种范围内的类型。
4.借助经典的父子类赋值关系(Father f = new Children()
)类比理解
将Java通配符的核心机制与 Father f = new Children()
这一经典的父子类赋值关系联系起来或许更易于理解。
对于 ? extends Type
(比如 ? extends Father
),它意味着容器里实际装的是 Father
或者像 Children
这样的子类对象。因此,当你从这个容器中读取 数据时,你得到的对象至少是一个 Father
,这就像 Father f = new Children()
这个赋值操作一样,是安全的向上转型,你可以放心地将读取到的对象当作 Father
来使用。
而对于 ? super Type
(比如 ? super Children
),容器里实际装的是 Children
或者像 Father
这样的父类对象。当你尝试读取 并希望得到 Children
时,这是不安全的,因为你拿到的可能是一个 Father
对象,这就像你不能直接将一个 Father
类型的引用 f
当作 Children
类型来用(需要不安全的向下转型)。然而,当你写入 一个 Children
对象时,却是安全的。为什么呢?因为无论这个容器实际是 List<Children>
、List<Father>
还是 List<Object>
,根据 父类引用 = 子类对象
的原则,它们都能合法地接收一个 Children
对象。所以,这个写入操作的安全性,同样源于那个基础的赋值规则在所有可能父类型上的普遍适用性。