**泛型(Generics)**让类和方法把“操作哪种类型”作为参数。它的主要价值是把错误提前到编译期,并减少不安全的强制 类型转换。
Table of contents
从类型安全仓库开始
import java.util.*;
interface 有标识<ID> { ID id(); }
final class 内存仓库<ID, T extends 有标识<ID>> {
private final Map<ID, T> data = new HashMap<>();
void 保存(T entity) { data.put(entity.id(), entity); }
Optional<T> 查找(ID id) { return Optional.ofNullable(data.get(id)); }
}
record 用户(Long id, String name) implements 有标识<Long> {}
T extends 有标识<ID> 是上界(Upper Bound):T 可以是该接口的任意实现,但必须能提供 ID。因此仓库无需
反射也能取得实体标识。
理解不变性
即使 Integer 是 Number 的子类,List<Integer> 也不是 List<Number> 的子类。否则调用方可以向“数字列表”加入
Double,破坏原列表只含整数的承诺。这叫泛型的不变性(Invariance)。
通配符用于表达只读或只写方向:
static double 求和(List<? extends Number> source) {
return source.stream().mapToDouble(Number::doubleValue).sum();
}
static void 加入默认值(List<? super Integer> target) {
target.add(0);
}
记忆 PECS:Producer Extends,Consumer Super。作为数据生产者的参数用 ? extends T;作为消费者用
? super T。若既读又写,通常直接使用 List<T>。
类型擦除带来的限制
Java 泛型主要在编译期工作,运行时多数类型参数会被擦除(Type Erasure)。因此不能写:
// new T()
// T.class
// new List<String>[10]
// if (value instanceof List<String>)
需要在运行时保留类型时,显式传入 Class<T>、工厂 Supplier<T>,或框架提供的类型令牌。例如:
static <T> T 创建(java.util.function.Supplier<T> factory) {
return factory.get();
}
var names = 创建(java.util.ArrayList<String>::new);
不要用原始类型 List 绕过编译器。它会把本应在写代码时发现的问题推迟成生产环境的 ClassCastException。
API 设计检查
- 类型参数名称简短但有含义:
T、ID、K、V。 - 只有一个方法需要类型参数时,优先写泛型方法而非泛型类。
- 不要为了“通用”暴露五六个类型参数;复杂签名常说明职责过多。
- 公共 API 不返回难以理解的嵌套通配符。
下一步
阅读Lambda、Stream 与副作用边界,把集合处理写成可读的数据管道。