Java 泛型:核心规则
泛型(Generics)把类型作为类、接口和方法的参数。List<String> 的价值是:写入不匹配的类型会在编译期失败,读取时不必重复强制转换。Java 泛型由类型擦除实现,因此也有明确的运行期边界。
示例省略
java.util与java.util.function的导入;标有“编译错误”的代码仅用于说明规则。
基础语法
<T> 是类型参数,String、Integer 是类型实参。泛型类可用同一个类型参数约束字段、参数和返回值:
1 | |
菱形运算符 <> 让编译器推断构造器的类型实参。常用命名为 T(类型)、E(元素)、K/V(键和值)、R(返回值)。
泛型方法的类型参数写在返回类型之前;它不要求所在类也是泛型类:
1 | |
静态成员不能使用类的类型参数,因为它不属于某个实例;需要时声明自己的类型参数:
1 | |
上界:约束类型能力
无界 T 只能按 Object 使用。算法依赖某种能力时,用 extends 声明上界:
1 | |
多个上界用 & 连接,最多一个类且必须排在第一位:<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> | T | T |
List<? extends T> | T | 仅 null |
List<? super T> | Object | T 及其子类 |
List<?> | Object | 仅 null |
1 | |
List<?> 表示元素类型未知,能接收任意 List,适合只遍历或输出的参数;它不同于 List<Object>。后者可以写任意引用,但不能接收 List<String>。
当未知类型必须在多个位置保持一致时,使用类型参数:
1 | |
输入参数通常适合通配符;字段和返回值通常应使用具体参数化类型。返回 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 | |
避免原始类型
原始类型是省略类型实参的遗留写法。它绕过编译器检查,可能把错误推迟为运行期 ClassCastException:
1 | |
新代码必须使用完整参数化类型。遇到 unchecked 警告,优先修复类型;不可避免的遗留边界应先验证输入,再在最小作用域使用 @SuppressWarnings("unchecked") 并说明安全理由。
API 速查
| 需求 | 推荐签名 |
|---|---|
| 只遍历任意列表 | List<?> |
读取 Number 或子类 | List<? extends Number> |
写入 Integer | List<? super Integer> |
| 读写同一种元素 | List<T> |
| 返回首个元素 | <T> T first(List<T>) |
创建 T | Supplier<? extends T> |
核心结论:泛型默认不变;输入参数按 PECS 选择通配符;运行期限制来自类型擦除。





