java泛型擦除是编译期将类型参数替换为object或上界并移除泛型逻辑,运行时仅存原始类型;但字节码中通过signature属性以字符串形式保留泛型签名,供反射、ide等工具还原类型信息。

Java 泛型类型擦除不是“彻底删除”,而是把类型参数从运行时逻辑中移除,同时把泛型结构以字符串形式保留在字节码的 Signature 属性 中——它不参与执行,但供反射、IDE、序列化等工具还原类型信息。
擦除是编译行为,Signature 是字节码“备忘录”
编译器把 List<string></string> 编译成 List(原始类型),所有泛型逻辑被抹去:不能 new T()、不能 instanceof List<string></string>、getClass() 也看不出区别。但与此同时,编译器会生成一条 JVM 标准属性:Signature,把它写进 class 文件对应位置(类、字段或方法),指向常量池里一个编码后的签名字符串,比如:
-
Ljava/util/List<ljava>;</ljava>(字段类型) -
Ljava/util/Map<ljava>;</ljava>(方法返回值) -
<ljava>(TT;)TT;</ljava>(泛型方法签名)
这个字符串不是 Java 源码,而是 JVM 规范定义的签名语法,用分号、尖括号和内部类名拼成,只用于描述结构,不触发任何运行时行为。
Signature 出现在三个关键位置
它不是全局一张表,而是按需附着在具体结构上:
-
类声明处:记录泛型类本身的类型变量和边界,如
class Box<t extends number></t>的T约束 -
字段定义处:保存字段的完整参数化类型,如
private List<string> items;</string>的List<string></string> -
方法定义处:涵盖方法自身的类型变量(
<k></k>)、参数类型、返回类型,如<t> T get()</t>
每个 Signature 属性本身很小,就两个字节:一个索引,指向常量池里真正的签名字符串。JVM 加载时忽略它,但 Field.getGenericType() 或 Method.getGenericReturnType() 这类反射调用,底层就是读这个字段再解析字符串。
为什么需要它?擦除后还能“找回”什么
因为擦除只影响运行时类型实例化,不销毁元数据。Signature 让以下场景成为可能:
- 框架做 JSON 反序列化时,通过
TypeReference<list>>{} </list>提取签名,还原目标泛型结构 - IDE 在编辑时提示
list.get(0)返回String,而不是Object - 子类继承
ArrayList<string></string>后,调用getGenericSuperclass()能拿到ArrayList<string></string>而非裸ArrayList
注意:它只对“声明位置明确”的泛型有效。局部变量、方法调用中的泛型实参(如 foo(new ArrayList<integer>())</integer>)不会进 Signature,也就无法在运行时还原。
混淆和 ASM 处理 Signature 的实际提醒
如果用 ProGuard/R8 做代码混淆,默认会删掉 Signature 属性——不是擦除出问题,是元数据被清理了。结果就是 getGenericType() 返回 Object 或原始类型。必须显式保留:
-
-keepattributes Signature(强制保留所有 Signature) - 配合
-keep class * extends com.google.gson.reflect.TypeToken { <init>(); }</init>(若用 TypeToken 模式)
用 ASM 解析时,直接看 visitField 或 visitMethod 回调里的 signature 参数即可;它就是 Signature 属性解码后的字符串,无需手动查常量池。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











