泛型(Generics)把类型作为类、接口和方法的参数。List<String> 的价值是:写入不匹配的类型会在编译期失败,读取时不必重复强制转换。Java 泛型由类型擦除实现,因此也有明确的运行期边界。

示例省略 java.util 与 java.util.function 的导入;标有“编译错误”的代码仅用于说明规则。

基础语法

<T> 是类型参数,String、Integer 是类型实参。泛型类可用同一个类型参数约束字段、参数和返回值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public final class Box<T> {
private T value;

public Box(T value) {
this.value = value;
}

public T get() {
return value;
}

public void set(T value) {
this.value = value;
}
}

Box<String> name = new Box<>("Ada");
String value = name.get();

菱形运算符 <> 让编译器推断构造器的类型实参。常用命名为 T(类型)、E(元素)、K/V(键和值)、R(返回值)。

泛型方法的类型参数写在返回类型之前;它不要求所在类也是泛型类:

1
2
3
4
5
6
public static <T> void swap(
List<T> list, int left, int right) {
T value = list.get(left);
list.set(left, list.get(right));
list.set(right, value);
}

静态成员不能使用类的类型参数,因为它不属于某个实例;需要时声明自己的类型参数:

1
2
3
4
5
6
7
class Cache<T> {
// private static T latest; // 编译错误

static <R> R identity(R value) {
return value;
}
}

上界:约束类型能力

无界 T 只能按 Object 使用。算法依赖某种能力时,用 extends 声明上界:

1
2
3
4
5
6
7
8
9
10
11
12
public static <T extends Number> double sum(List<T> values) {
double total = 0;
for (T value : values) {
total += value.doubleValue();
}
return total;
}

public static <T extends Comparable<T>> T larger(
T left, T right) {
return left.compareTo(right) >= 0 ? left : right;
}

多个上界用 & 连接,最多一个类且必须排在第一位:<T extends Number & Comparable<T>>。没有 <T super Number>;下界只用于通配符。

不变性与 PECS

即使 Dog extends Animal,List<Dog> 也不是 List<Animal>。否则可以经由 List<Animal> 写入 Cat,破坏原列表只能保存 Dog 的约束。

需要接收不同子类型集合时,用通配符。判断原则是 PECS:从参数读取时用 extends,向参数写入时用 super。

参数类型可读取为可写入
List<T>TT
List<? extends T>T仅 null
List<? super T>ObjectT 及其子类
List<?>Object仅 null
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static double sum(List<? extends Number> values) {
double total = 0;
for (Number value : values) {
total += value.doubleValue();
}
return total;
}

static void addScores(List<? super Integer> target) {
target.add(90);
target.add(100);
}

public static <T> void copy(
List<? super T> destination,
List<? extends T> source) {
for (int i = 0; i < source.size(); i++) {
destination.set(i, source.get(i));
}
}

List<?> 表示元素类型未知,能接收任意 List,适合只遍历或输出的参数;它不同于 List<Object>。后者可以写任意引用,但不能接收 List<String>。

当未知类型必须在多个位置保持一致时,使用类型参数:

1
2
3
public static <T> T first(List<T> values) {
return values.get(0);
}

输入参数通常适合通配符;字段和返回值通常应使用具体参数化类型。返回 List<?> 会把未知类型问题转嫁给每个调用方。

类型擦除及其限制

编译器先检查泛型规则,再擦除类型参数以保持二进制兼容:无界 T 擦除为 Object,T extends Number 擦除为 Number,List<String> 擦除为 List。编译器会在读取处补上必要的类型转换。

因此,List<String> 和 List<Integer> 不会生成不同的运行期类,对象也不保存自己的类型实参。由此产生常见限制:

  • 类型实参必须是引用类型:使用 List<Integer>,不能使用 List<int>。
  • 不能直接 new T():应让调用方提供 Supplier<? extends T>。
  • 不能创建参数化类型数组:使用 List<List<String>> 代替 new List<String>[10]。
  • 不能检查 instanceof List<String>:只能检查 instanceof List<?>,并在不可信输入边界逐项验证元素。
  • 不能仅靠类型实参重载:save(List<String>) 和 save(List<Integer>) 擦除后冲突。
1
2
3
4
5
6
7
8
public static <T> List<T> createMany(
int count, Supplier<? extends T> supplier) {
List<T> result = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
result.add(supplier.get());
}
return result;
}

避免原始类型

原始类型是省略类型实参的遗留写法。它绕过编译器检查,可能把错误推迟为运行期 ClassCastException:

1
2
3
4
5
List raw = new ArrayList();
raw.add("not a number");

List<Integer> numbers = raw; // unchecked 警告
Integer number = numbers.get(0);

新代码必须使用完整参数化类型。遇到 unchecked 警告,优先修复类型;不可避免的遗留边界应先验证输入,再在最小作用域使用 @SuppressWarnings("unchecked") 并说明安全理由。

API 速查

需求推荐签名
只遍历任意列表List<?>
读取 Number 或子类List<? extends Number>
写入 IntegerList<? super Integer>
读写同一种元素List<T>
返回首个元素<T> T first(List<T>)
创建 TSupplier<? extends T>

核心结论:泛型默认不变;输入参数按 PECS 选择通配符;运行期限制来自类型擦除。