Java 泛型的“擦除”与“保留”:一次完整的编译与反编译实验
Java 的泛型常常被称为“伪泛型”,因为运行时类型系统中是擦除式泛型。然而,我们又能在框架(如 Jackson 的 TypeReference
、Guava 的 TypeToken
)中看到它们精准地识别出复杂的泛型结构。这背后到底发生了什么?
本文通过一个小实验,带大家从源码、编译、字节码、反编译、再到反射调用,完整地理解 泛型擦除的时机 与 泛型信息保留的方式。
一、准备实验代码
我们先自己写一个简化版的 TypeReference
(避免引入 Jackson 依赖),核心思想就是:在匿名子类的父类签名里,编译器会留下泛型信息,我们在构造器里把它读出来。
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;/*** 简化版的 TypeReference*/
abstract class TypeReference<T> {protected final Type type;protected TypeReference() {Type superClass = getClass().getGenericSuperclass();this.type = ((ParameterizedType) superClass).getActualTypeArguments()[0];}public Type getType() {return type;}
}public class Demo {public static void main(String[] args) {TypeReference<List<String>> ref = new TypeReference<List<String>>() {};System.out.println("Captured type = " + ref.getType());}
}
运行后输出:
Captured type = java.util.List<java.lang.String>
二、编译生成 class 文件
用 JDK 编译:
javac Demo.java
会生成两个字节码文件:
Demo.class # 主类
Demo$1.class # 匿名子类(new TypeReference<...>() {})
三、字节码观察
1. 主类(Demo.class)
反编译:
javap -c Demo.class
关键片段:
0: new #2 // class per/mjn/webflux_demo/Demo$1
3: dup
4: invokespecial #3 // Method per/mjn/webflux_demo/Demo$1."<init>":()V
7: astore_1
可以看到,这里只是在创建 Demo$1
实例,类型信息完全擦除了,看不到 <List<String>>
。
2. 匿名类(Demo$1.class)
反编译:
javap -v Demo$1.class
输出片段:
class per.mjn.webflux_demo.Demo$1 extends per.mjn.webflux_demo.TypeReference<java.util.List<java.lang.String>>Signature: #9 // Lper/mjn/webflux_demo/TypeReference<Ljava/util/List<Ljava/lang/String;>;>;
这里就是关键证据:
- 类头部:显示了
extends TypeReference<List<String>>
。 - Signature 属性:
LTypeReference<Ljava/util/List<Ljava/lang/String;>;>;
这是编译器在 class 文件中额外写入的泛型元数据。
四、运行时反射的利用
在 TypeReference
构造器中,我们调用:
Type superClass = getClass().getGenericSuperclass();
这个方法正是读取了 class 文件里的 Signature 元数据,然后把泛型参数还原成 ParameterizedType
,所以可以打印出 List<String>
。
五、整个过程的时序梳理
如下图所示:
六、总结
-
泛型擦除的时机:
在 编译期,生成字节码前,所有泛型参数都被擦除为原始类型(如Object
)。 -
泛型信息保留的时机:
编译器在写.class
文件时,会在 Signature 属性里附带泛型参数信息。 -
运行时 JVM 类型系统:
JVM 并不会在运行时保留泛型参数作为真正的类型信息,所有的类型检查都依赖编译期擦除后的原始类型。 -
反射如何利用:
通过getGenericSuperclass()
、getGenericInterfaces()
等方法,能读取 class 文件中的 Signature,从而还原完整泛型结构。这就是TypeReference
、TypeToken
等工具的原理。
七、启发
- Java 的泛型并不是彻底消失:擦除用于运行时类型检查,Signature 保留用于反射与工具框架。
- 熟悉这套机制,能帮助我们理解:为什么
List<String>
在运行时是ArrayList
,却又能被 Jackson 精确反序列化成List<Map<String,Integer>>
。
这个小实验展示了 Java 泛型的“擦除”与“保留”如何共存。理解这一点,不仅能解答“TypeReference 为何能拿到泛型参数”的疑惑,也能帮助我们写出更强大的框架级代码。