1. 泛型的不变性:List<Number> ≠ List<Integer>
在 Java 中,泛型是严格的:它们是不变的。这意味着即使一个类型是另一个类型的子类型(例如,Integer 是 Number 的子类型),使用这些类型的集合彼此之间也没有关系。
想象一下:你有一个装苹果的盒子(List<Integer>),而有人说它其实是“装水果的盒子”(List<Number>)。看起来好像有道理,因为苹果是水果。但这样就可以往这个盒子里放香蕉(Double),一切就会被破坏——这个盒子实际上是“只装苹果”的。
代码示例:
List<Integer> intList = new ArrayList<>();
List<Number> numList = intList; // 编译错误!
编译器在这里特意保持严格:它不允许把“苹果”的列表变成“水果”的列表。否则我们就可以往里面添加任何东西,程序会在运行时崩溃。
因此,List<Number> 和 List<Integer> 是两个完全不同的类型,尽管 Integer 本身是 Number 的子类型。
与数组不同:
Integer[] intArr = new Integer[10];
Number[] numArr = intArr; // 允许(数组是协变的)
numArr[0] = 3.14; // 运行时错误 (ArrayStoreException)
结论:泛型是不变的,以确保在编译期的类型安全。
2. 类型参数的边界
有时需要限制泛型类或方法可以处理哪些类型。为此使用边界(bounds)。
上界(extends)
class Stats<T extends Number> {
private T[] nums;
// ...
}
现在 Stats 只能与 Number 的子类型一起工作(Integer、Double、Float 等)。尝试创建 Stats<String> 会导致编译错误。
与接口的约束
class Sorter<T extends Comparable<T>> {
void sort(List<T> list) { /* ... */ }
}
现在 Sorter 只能与实现了 Comparable 接口的类型一起工作。
多重边界
可以通过 & 同时指定多个约束:
class MyClass<T extends Number & Comparable<T>> { /* ... */ }
T 必须是 Number 的子类,并且实现 Comparable<T>。
顺序很重要:先写类,再写接口。
3. 认识通配符:?
通配符(Wildcard)是一种“占位”的类型,表示:“这里可以是某个类型,但我不知道具体是哪一种。”
示例:
- List<?> —— 任意类型的列表。
- List<? extends Number> —— 任何 Number 子类型(如 Integer、Double)的列表。
- List<? super Integer> —— 任何 Integer 的父类型(如 Integer、Number、Object)的列表。
为什么需要通配符?
它们允许编写能与不同但相关类型的集合一起工作的泛型方法。
示例:只读(producer)
void printNumbers(List<? extends Number> list) {
for (Number n : list) {
System.out.println(n);
}
// list.add(123); // 错误!不能添加元素
}
- 可以将元素作为 Number 来读取。
- 不能添加元素(除了 null)。
示例:只写(consumer)
void addIntegers(List<? super Integer> list) {
list.add(42); // OK
// Integer x = list.get(0); // 错误!我们不知道会返回什么类型
}
- 可以添加 Integer(或其子类型)的元素。
- 不能安全地将元素作为 Integer 读取(只能当作 Object 读取)。
PECS 规则
PECS —— “Producer Extends, Consumer Super”:
- Producer — Extends:如果集合只提供数据(producer),使用 ? extends T。
- Consumer — Super:如果集合只接收数据(consumer),使用 ? super T。
好记法:extends —— 用于读取(producer),super —— 用于写入(consumer)。
对比:数组是协变的,泛型是不变的
- 数组是协变的:可以把 Integer[] 赋值给 Number[] 类型的变量。
- 泛型是不变的:不能把 List<Integer> 赋值给 List<Number> 类型的变量。
通配符可以在一定程度上“缓解”泛型的不变性。
5. 泛型方法与类型推断;限制(类型擦除)
泛型方法
可以声明与任意类型一起工作的泛型方法:
public static <T> void printList(List<T> list) {
for (T elem : list) {
System.out.println(elem);
}
}
类型推断(type inference)
Java 可以“推断”类型参数:
List<String> strings = List.of("a", "b");
printList(strings); // T = String
泛型的限制:类型擦除
- 在 Java 中,泛型通过类型擦除实现:编译后会删除类型参数的信息。
- 在字节码中,List<String> 与 List<Integer> 没有差别。
- 不能创建泛型类型的数组:new List<String>[10] —— 错误。
- 不能对带类型参数的实例使用 instanceof:obj instanceof List<String> —— 错误。
6. 集合与 Stream API 实战
在列表之间复制元素
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (T item : src) {
dest.add(item);
}
}
- src —— producer(? extends T)
- dest —— consumer(? super T)
用法:
List<Integer> ints = List.of(1, 2, 3);
List<Number> nums = new ArrayList<>();
copy(nums, ints); // OK: Number 是 Integer 的超类型
Stream API 与通配符
List<Integer> ints = List.of(1, 2, 3);
List<? extends Number> numbers = ints;
numbers.stream()
.map(Number::doubleValue)
.forEach(System.out::println);
使用通配符的打印
public static void printAll(List<?> list) {
for (Object o : list) {
System.out.println(o);
}
}
7. 常见错误
错误 №1:Raw types(原始类型)。
使用“原始”类型会关闭类型检查,并导致运行时错误。
List list = new ArrayList(); // raw type — 不好!
list.add("String");
list.add(123); // 可以添加任何东西
String s = (String) list.get(1); // ClassCastException!
不要使用 raw types。务必声明类型参数:List<String>、List<Integer>。
错误 №2:带有 extends 的不安全操作。
尝试向带有 ? extends ... 的集合添加元素会导致编译错误。
List<? extends Number> nums = new ArrayList<Integer>();
nums.add(3.14); // 编译错误!
错误 №3:类型擦除导致的重载冲突。
不能仅根据泛型参数来重载方法——类型擦除后签名会相同。
public void process(List<String> list) { /* ... */ }
public void process(List<Integer> list) { /* ... */ } // 编译错误!
错误 №4:泛型数组。
由于类型擦除,不能创建参数化类型的数组。
List<String>[] arr = new List<String>[10]; // 编译错误!
GO TO FULL VERSION