Java泛型编译时擦除,为什么运行时还能通过反射能拿到?

Java 泛型在‌编译时完成类型擦除‌,但运行时仍可通过反射获取部分泛型信息,核心在于‌字节码中保留了声明侧的泛型元数据‌,而使用侧信息会被完全擦除。以下是具体机制分析:


一、类型擦除的时机和范围

  1. 编译时擦除
    Java 编译器(javac)在生成字节码(.class 文件)时:

    • 替换泛型类型为原始类型(如 List<String> → List)‌
    • 擦除‌方法体内部‌的泛型类型信息(如局部变量、new T() 等)‌
    • 插入强制类型转换(如 (String) list.get(0))‌
  2. 运行时无使用侧信息
    JVM 执行时:

    • 无法识别如 List<String> 和 List<Integer> 的区别(list.getClass() 结果相同)‌
    • 不能通过反射获取局部变量、实例化对象的泛型类型‌‌

二、运行时获取泛型信息的原理

以下场景的泛型信息会以元数据形式保留在字节码的 Signature 属性中,供反射读取:

  1. 类/接口的泛型声明

    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"));
    }
  2. 字段的泛型类型

    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));
    }
  3. 方法返回值/参数的泛型

    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));
    }
  4. 继承父类的具体泛型参数

    
    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 类型 ‌
}

四、设计本质:擦除与保留的平衡

  1. 擦除目的

    • 兼容旧版 JVM(无需修改字节码结构)‌
    • 避免运行时为每个泛型创建新类(节省内存)‌
  2. 保留声明侧信息的原因

    • 支持编译器静态类型检查
    • 为反射等高级特性提供有限元数据(如 JSON 解析、ORM 框架)‌
  3. 元数据存储位置

    • 保留在 .class 文件的 Signature 属性中(非执行代码)‌
    • JVM 加载类时解析该属性到内存供反射 API 调用 ‌

五、总结:擦除与反射的共存机制

阶段泛型信息状态反射支持性
编译前完整泛型类型(源码)不适用
编译后声明侧泛型存入 Signature 属性可获取 ‌
运行时使用侧泛型被擦除不可获取 ‌

📌 核心结论:
反射获取的是编译时预设的泛型元数据‌(如类/字段/方法的泛型签名),而非运行时动态类型‌。这解释了为何二者能共存:编译器在擦除方法体泛型的同时,选择性保留了声明侧的元信息以满足部分反射需求。