一、Java 泛型擦除的本质 Java 的泛型是一种编译期语法糖,并非原生支持的语言特性。为了兼容 JDK 1.5 之前没有泛型的代码,编译器会在编译阶段执行类型擦除(Type Erasure): 1. 擦除泛型参数 所有泛型类型参数(如 ``、``)会被替换为它们的上界类型: - 未指定上界时,默认擦除为 `Object`。 例如 `List` → 编译后变为 `List`,`add` 方法变成 `add(Object)`,`get` 方法返回 `Object`。 - 指定上界时,擦除为上界类型。 例如 `List` → 编译后擦除为 `List`。 2. 插入强制类型转换 为了保证代码在运行时的类型安全,编译器会在读取泛型元素的地方自动插入强制类型转换。 例如 `String s = list.get(0);` 编译后会变成 `String s = (String) list.get(0);`。 核心结果: 运行时 JVM 只能看到原始类型(如 `List`、`Map`),无法直接感知到 `String`、`Integer` 这类泛型参数信息。 二、为什么还能通过反射获取部分泛型信息? 虽然大部分泛型信息被擦除了,但有一部分结构级别的泛型信息会被编译器刻意保留,并写入 `.class` 字节码文件的 `Signature` 属性中。 1. 哪些泛型信息会被保留? 只有定义在类、接口、成员变量、方法签名上的泛型信息会被保留,具体包括: - 类/接口的父类/实现接口声明 例如 `public class MyList extends ArrayList`,`ArrayList` 这个带泛型的父类信息会被保留。 - 类中成员变量(Field)的泛型类型 例如 `private List scores;`,`List` 的泛型信息会被保留。 - 方法(Method)的参数类型与返回值类型 例如 `public List getNames(List ids)`,参数和返回值的泛型信息会被保留。 2. 为什么这些信息会被保留? - 为了保证编译器的类型检查:IDE 和编译器需要这些信息来检查代码的类型安全。 - 为了支持反射 API:JDK 设计了专门的反射接口来读取这些信息,保证框架(如 Spring、Jackson)能基于泛型做自动化处理。 - 这些信息保存在字节码的 `Signature` 属性中,不会影响运行时的兼容性。 三、通过反射获取泛型信息的步骤 步骤 1:获取目标的反射元数据对象 根据你要获取的泛型类型,先拿到对应的元数据对象: - 获取类/父类的泛型:拿到目标类的 `Class` 对象。 - 获取成员变量的泛型:拿到目标变量的 `Field` 对象。 - 获取方法参数/返回值的泛型:拿到目标方法的 `Method` 对象。 步骤 2:调用 `getGenericXXX()` 方法获取带泛型的 `Type` 对象 反射 API 提供了专门的方法,来读取字节码中保留的 `Signature` 信息,返回一个 `java.lang.reflect.Type` 对象(它是所有类型的通用接口): - 父类:`Class.getGenericSuperclass()` - 实现接口:`Class.getGenericInterfaces()` - 成员变量类型:`Field.getGenericType()` - 方法返回值类型:`Method.getGenericReturnType()` - 方法参数类型:`Method.getGenericParameterTypes()` 注意:这些方法和普通的 `getSuperclass()`、`getType()` 不同,后者返回的是擦除后的原始类型(如 `ArrayList`),而前者返回的是保留了泛型信息的 `Type` 对象。 步骤 3:判断并解析 `ParameterizedType`(参数化类型) `Type` 是一个空接口,它的一个重要实现是 `ParameterizedType`,用来表示带泛型的参数化类型(如 `List`、`Map`)。 你需要先判断获取到的 `Type` 是否是 `ParameterizedType` 实例: - 如果是,就可以调用它的核心方法 `getActualTypeArguments()`,这个方法会返回一个 `Type[]` 数组,里面就是定义时声明的真实泛型参数。 - 数组里的每个元素,都对应着原始声明中的泛型参数(如 `List` 会返回一个长度为 1 的数组,元素为 `String`;`Map` 会返回长度为 2 的数组,分别为 `K` 和 `V`)。 步骤 4:递归解析嵌套泛型(如 `List>`) 如果泛型本身还是一个参数化类型(如 `List>`),`getActualTypeArguments()` 返回的 `Type` 仍然是一个 `ParameterizedType`,你可以重复步骤 3,递归解析嵌套的泛型信息。 四、不同场景下的获取能力对比 | 场景 | 示例 | 能否通过反射获取泛型信息? | 原因 | | 类继承的父类泛型 | `class A extends B` | ✅ 能 | 信息保存在类的 `Signature` 属性中 | | 类实现的接口泛型 | `class C implements D` | ✅ 能 | 信息保存在类的 `Signature` 属性中 | | 成员变量的泛型 | `private List list;` | ✅ 能 | 信息保存在字段的 `Signature` 属性中 | | 方法返回值的泛型 | `public List getList()` | ✅ 能 | 信息保存在方法的 `Signature` 属性中 | | 方法参数的泛型 | `public void setList(List list)` | ✅ 能 | 信息保存在方法的 `Signature` 属性中 | | 方法内局部变量的泛型 | `public void test() { List l = new ArrayList<>(); }` | ❌ 不能 | 局部变量的泛型信息编译后完全擦除,无任何保留 | | 直接创建的泛型对象 | `new ArrayList()` | ❌ 不能 | 仅在编译期有效,运行时没有任何元数据 | 五、局限性与注意事项 1. 只能获取定义时声明的泛型,无法获取运行时传入的类型 例如 `List list = new ArrayList<>();`,反射无法知道 `list` 里存的是 `String`,只能知道 `list` 这个变量声明时的类型是 `List`。 2. 无法获取通配符泛型的具体类型 例如 `List`,反射能知道它的上界是 `Number`,但无法知道运行时实际传入的具体子类型。 3. 类型变量(如 ``)的解析需要上下文 如果是未绑定的类型变量(如 `class Box` 中的 `T`),反射只能获取到变量名,无法知道它的具体类型,除非它被绑定到了具体的类上(如 `class StringBox extends Box`)。 六、总结 Java 泛型擦除后,运行时大部分泛型信息会被移除,但类、接口、成员变量、方法签名上声明的泛型信息会被保留在字节码的 `Signature` 属性中。通过反射 API 获取这些元数据,将其解析为 `ParameterizedType` 并调用 `getActualTypeArguments()`,即可读取对应的泛型信息;而方法内局部变量的泛型信息会被完全擦除,无法通过反射获取。