Java泛型擦除是编译期系统性替换:类型参数按上界替换成Object或边界类型,引用降级为原始类型,编译器插入强制转换,静态上下文清空,运行时无泛型信息。

泛型类型擦除不是“删代码”,而是编译器在生成字节码前,把所有泛型参数替换成具体类型(Object 或上界),并插入隐式强转——运行时 JVM 完全看不到 <String> 这类信息。攻克它,关键不在背规则,而在亲手验证“擦除后什么变了、什么不能做、为什么不能”。
盯住擦除的三处真实表现
不看文档,直接写这几行代码跑一遍,印象立刻加深:
-
getClass() 返回原始类型:new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass() → true。两者字节码完全一样,都是
ArrayList。 -
instanceof 只能判原始类型:List<String> list = new ArrayList<>(); list instanceof List → 合法;list instanceof List<String> → 编译报错。因为运行时没有
List<String>这个类型。 - 泛型数组创建被禁止:T[] arr = new T[5]; → 编译失败;必须写成 T[] arr = (T[]) new Object[5]; → 警告但能过。这是因为 new 操作需要确切的运行时类型,而 T 已被擦成 Object。
理解擦除背后的两个硬约束
很多“不能做”其实都源于同一个事实:JVM 不认识泛型,所有泛型实例共享同一份字节码。
当用户要求为 Java 代码添加注释、修改注释、审查注释、整理注释格式,或要求按照代码注释规范处理 Java 文件时,使用本技能。本技能用于统一生成和维护 Java 类注释模板、按 Git 对比 release 判断版本号、补充每行代码上方中文块注释、调整注释空行规则,并保持 Java 文件 UTF-8 无 BOM 编码。典型触发场景包括:补充类注释、更新 @version、检查 @author 来源、整理行尾注释、把注释放到代码上一行、检查注释是否符合规范、构建前检查 Java 注释风格。
- 静态成员不能用泛型参数:static void print(T t) { } 是非法的。因为 static 属于类,而 T 属于实例;擦除后所有 Box<String>、Box<Integer> 都是同一个 Box 类,T 早已不存在。
- 不能 new T() 或 new T[]:构造器调用和数组创建都依赖运行时类型信息,而擦除后只剩 Object,编译器无法确认 T 是否有无参构造、是否是引用类型。
通配符与擦除的联动逻辑
PECS(Producer Extends, Consumer Super)不是口诀,是擦除限制下的安全读写策略:
立即学习“Java免费学习笔记(深入)”;
- ? extends Number:只能读,不能 add(除了 null)。因为擦除后底层是 Number,你 add(1) 看似合理,但实际可能指向 ArrayList<Integer>,而它不允许存 Float —— 编译器为保安全,一律禁止写。
- ? super Integer:只能写,读出来是 Object。因为擦除后底层是 Object 或其父类,add(Integer) 总是安全的;但 get() 返回的是上界类型(如 Object),无法保证是 Integer。
反射中如何应对擦除
运行时拿不到泛型实参,但可以通过方法/字段的声明位置反推:
- 用
Method.getGenericReturnType()或Field.getGenericType()获取带泛型的 Type 对象(如 ParameterizedType)。 - 对泛型方法,需配合
Method.getActualTypeArguments()提取实参(注意:仅对方法定义中的泛型有效,对调用时传入的泛型无效)。 - 常见坑:
list.getClass().getGenericSuperclass()可拿到 ArrayList<E> 的 E,但new ArrayList<String>().getClass()...拿不到 String —— 因为擦除发生在实例化之后。

















