Java 泛型在编译时完成类型擦除,但运行时仍可通过反射获取部分泛型信息,核心在于字节码中保留了声明侧的泛型元数据,而使用侧信息会被完全擦除。以下是具体机制分析:
一、类型擦除的时机和范围
-
编译时擦除
Java 编译器(javac)在生成字节码(.class文件)时:- 替换泛型类型为原始类型(如
List<String>→List) - 擦除方法体内部的泛型类型信息(如局部变量、
new T()等) - 插入强制类型转换(如
(String) list.get(0))
- 替换泛型类型为原始类型(如
-
运行时无使用侧信息
JVM 执行时:- 无法识别如
List<String>和List<Integer>的区别(list.getClass()结果相同) - 不能通过反射获取局部变量、实例化对象的泛型类型
- 无法识别如
二、运行时获取泛型信息的原理
以下场景的泛型信息会以元数据形式保留在字节码的 Signature 属性中,供反射读取:
-
类/接口的泛型声明
public class MyList<T> {} // 类声明保留泛型信息@Test public void test1() {// 通过反射拿到类声明的泛型类型TypeVariable<?>[] typeParameters = MyList.class.getTypeParameters();assertThat(typeParameters.length, is(1));assertThat(typeParameters[0].getTypeName(), is("T"));assertThat(typeParameters[0].getBounds().length, is(1));assertThat(typeParameters[0].getBounds()[0].getTypeName(), is("java.lang.Object")); } -
字段的泛型类型
public List<String> names; // 字段类型保留泛型信息@Test public void test() throws NoSuchFieldException {// 通过反射获取字段的泛型类型Type type = GenericReflectTest.class.getField("names").getGenericType();assertThat(type, instanceOf(ParameterizedType.class));ParameterizedType paramType = (ParameterizedType) type;assertThat(List.class, is(paramType.getRawType()));assertThat(paramType.getActualTypeArguments().length, is(1));assertThat(paramType.getActualTypeArguments()[0], is(String.class)); } -
方法返回值/参数的泛型
public List<Integer> getNumbers() { ... } // 方法返回值保留泛型@Test public void test2() throws NoSuchMethodException {// 通过反射获取方法的泛型返回类型Type type = GenericReflectTest.class.getMethod("getNumbers").getGenericReturnType();assertThat(type, instanceOf(ParameterizedType.class));ParameterizedType paramType = (ParameterizedType) type;assertThat(List.class, is(paramType.getRawType()));assertThat(paramType.getActualTypeArguments().length, is(1));assertThat(paramType.getActualTypeArguments()[0], is(Integer.class)); } -
继承父类的具体泛型参数
private static class MyList2 extends MyList<String> {} // 父类泛型参数保留@Test public void test3() {Type type = MyList2.class.getGenericSuperclass();ParameterizedType paramType = (ParameterizedType) type;assertThat(MyList.class, is(paramType.getRawType()));assertThat(paramType.getActualTypeArguments().length, is(1));assertThat(paramType.getActualTypeArguments()[0], is(String.class)); }
三、关键限制:反射无法获取的场景
| 场景 | 示例 | 原因 |
|---|---|---|
| 局部变量泛型 | List<String> list = ... | 字节码中无类型信息 |
| 实例化对象类型 | new ArrayList<String>() | 运行时视为原始类型 |
| 泛型类未指定具体参数时 | class MyList<T> { T item; } | item 的实际类型未知 |
⚠️ 示例:以下代码无法通过反射获取
T的类型public class Box<T> { private T value; // 反射只能获取到 Object 类型 }
四、设计本质:擦除与保留的平衡
-
擦除目的
- 兼容旧版 JVM(无需修改字节码结构)
- 避免运行时为每个泛型创建新类(节省内存)
-
保留声明侧信息的原因
- 支持编译器静态类型检查
- 为反射等高级特性提供有限元数据(如 JSON 解析、ORM 框架)
-
元数据存储位置
- 保留在
.class文件的Signature属性中(非执行代码) - JVM 加载类时解析该属性到内存供反射 API 调用
- 保留在
五、总结:擦除与反射的共存机制
| 阶段 | 泛型信息状态 | 反射支持性 |
|---|---|---|
| 编译前 | 完整泛型类型(源码) | 不适用 |
| 编译后 | 声明侧泛型存入 Signature 属性 | 可获取 |
| 运行时 | 使用侧泛型被擦除 | 不可获取 |
📌 核心结论:
反射获取的是编译时预设的泛型元数据(如类/字段/方法的泛型签名),而非运行时动态类型。这解释了为何二者能共存:编译器在擦除方法体泛型的同时,选择性保留了声明侧的元信息以满足部分反射需求。