泛型擦除与可具体化边界
泛型类型擦除如何工作:运行时可见什么、为何不能 new T[],以及桥接方法直觉。
#concept
#growing
#dev
#java
[!info] related notes
泛型擦除与可具体化边界
这个概念为什么出现
学了泛型仍会对 List<String> 运行时类型、异常捕获、数组创建感到困惑。根因是 Java 泛型主要是编译期机制,采用类型擦除兼容历史虚拟机。
[!abstract] 一句话理解 类型变量擦除为最左边的上界(无显式上界时为 Object);同一泛型类的不同参数化共享运行时类,因此不能依赖
T做可具体化操作,数组与异常等有专门限制。
最小可运行示例
场景:运行时看两个参数化列表的类
import java.util.*;
public class ErasurePeek {
public static void main(String[] args) {
List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass()); // true
Object unknown = a;
// unknown instanceof List<String> // 编译错误:无法安全检查元素类型
System.out.println(unknown instanceof List<?>);
}
}
javac ErasurePeek.java && java ErasurePeek
期望:
true
true
结合场景再看三个关注点
- 运行时类相同不代表编译期类型相同。
- 强制转换由编译器插入,不安全用法会在运行期炸。
- 类型令牌表达的是类型描述,不会自动验证集合内每个元素。
核心概念与准确模型
1. 擦除规则直觉
- 无界 → Object
T extends Number→ Number;交叉上界如T extends Number & Comparable<T>也擦除为最左边的 Number。- 桥接方法保持多态
2. 不可做的事
new T()/new T[]- 泛型类不能继承
Throwable;catch (T e)不能捕获类型变量,但throws T(T extends Throwable)可以合法。 Object不能用instanceof List<String>检查。Java 16+ 允许不需要 unchecked 转换的参数化检查,例如静态类型为List<String>的a instanceof ArrayList<String>;这只检查实现类,不检查元素。
3. reification
可具体化(reifiable)类型包括基本类型、非泛型类、raw type、仅使用无界通配符的参数化类型(如 List<?>),以及元素类型可具体化的数组。new List<?>[n] 合法,new List<String>[n] 和 new T[n] 不合法;后两种数组类型仍可出现在声明中。
边界情况与反直觉行为
- 反射
getGeneric*可看到签名信息(类文件属性),不等于完整运行时 reify。 - 原始类型触发未检查警告。
- 非可具体化类型的泛型变参可能导致 heap pollution;不是每次调用都会污染。
常见误区
[!warning] 常见误区:认为擦除=泛型没用 编译期安全与 API 表达仍然极有价值。
工程实践
- 避免 raw type。
Class<T>可传递运行时类,但没有List<String>.class;超类型令牌(如库提供的TypeToken)可捕获声明中的嵌套类型参数,不能据此保证任意对象的内容符合声明。- 抑制警告仅限边界。
- 读 javap 看桥接方法建立直觉。
本节总结
- 擦除解释运行时限制
- 编译期仍强约束
- 与数组 reify 对比记忆
自测题
- 为什么不能创建泛型数组
new T[n]? - 桥接方法为了什么?
参考答案
- 擦除与数组可具体化冲突,类型安全难保证。
- 在擦除后仍保持覆盖与调用的二进制兼容/多态分派。
延伸阅读与资料来源
| 资料 | 类型 | 支撑内容 |
|---|---|---|
| Type Erasure Tutorial | 教程 | 擦除 |
| JLS 4.6 Type Erasure | 规范 | 规则 |
| JLS 15.20.2 instanceof | 规范 | Java 16 起的参数化类型检查边界 |