Java 泛型中的 `? super T` 和 `extends T` 是两种不同的通配符表达式,它们用于在泛型的上下界限定中提供更大的灵活性。理解这两种通配符的关键在于理解它们对类型参数的限制。
### 1. `extends T`
`extends T` 通配符表示一个类型参数,它必须是类型 `T` 或者 `T` 的任何子类。当我们使用 `List<Number>`,这意味着我们可以存储 `Number` 类或其任何子类(如 `Integer`, `Double` 等)的对象。这种类型的通配符在方法参数中常见,用于表示方法可以接受任何类型 `T` 或其子类的参数。
例如:
```java
public static void printList(List<? extends Number> numbers) {
for (Number n : numbers) {
System.out.println(n);
}
}
// 调用示例
printList(Arrays.asList(1, 2, 3)); // Integer
printList(Arrays.asList(1.0, 2.0, 3.0)); // Double
```
### 2. `super T`
`super T` 通配符则相反,它表示一个类型参数,它必须是类型 `T` 或者 `T` 的任何父类。这在需要从列表中取出元素并进行操作,但不需要插入新元素时很有用。`super T` 通常出现在方法的返回类型中。
例如:
```java
public static <T> void consumeNumbers(List<? super Number> numbers) {
numbers.add(1); // 不允许,因为具体的类型未知
// 可以安全地取出并操作元素
Number n = numbers.get(0);
System.out.println(n);
}
// 调用示例
consumeNumbers(new ArrayList<Object>()); // 允许,因为 Object 是所有类型的父类
consumeNumbers(new ArrayList<Number>()); // 也允许,因为 Number 是 Number 的父类
```
### 区别与应用场景
- **读取(只读操作)**:`extends T` 更适合读取操作,因为你可以从列表中安全地取出 `T` 或其子类的实例。
- **写入(只写操作)**:`super T` 适合写入操作,因为它允许你向列表中添加 `T` 或其父类的实例,但不能读取具体的类型。
- **多态性**:`extends T` 表现为协变,这意味着 `List<Number>` 是 `List<Integer>` 的子类型,因此你可以在接受 `List<Number>` 参数的方法中传递 `List<Integer>` 实例。而 `super T` 并不具有这种协变性。
- **类型安全**:`super T` 在插入元素时提供更强的类型安全性,因为只能添加 `T` 或其父类的实例,防止了不兼容类型的对象被加入列表。
### 数组的协变
在 Java 中,数组具有协变性,意味着如果 `B` 是 `A` 的子类,那么 `B[]` 也是 `A[]` 的子类。然而,这种协变性可能导致运行时错误,如示例代码所示,当你尝试将 `Fruit` 对象存入 `Apple[]` 时,会抛出 `ArrayStoreException`。这是因为虽然数组类型上是协变的,但在实际存储时,数组仍然保持着它的原始类型,即只能存储创建时指定的类型或其子类。
总结来说,理解和掌握 `? super T` 和 `extends T` 在泛型中的应用对于编写类型安全且灵活的 Java 代码至关重要。正确使用这些通配符可以提高代码的可读性和可维护性,并减少类型转换的需要。