You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
84 lines
7.2 KiB
84 lines
7.2 KiB
一、Java 泛型擦除的本质
|
|
Java 的泛型是一种编译期语法糖,并非原生支持的语言特性。为了兼容 JDK 1.5 之前没有泛型的代码,编译器会在编译阶段执行类型擦除(Type Erasure):
|
|
|
|
1. 擦除泛型参数
|
|
所有泛型类型参数(如 `<T>`、`<K, V>`)会被替换为它们的上界类型:
|
|
- 未指定上界时,默认擦除为 `Object`。
|
|
例如 `List<String>` → 编译后变为 `List`,`add` 方法变成 `add(Object)`,`get` 方法返回 `Object`。
|
|
- 指定上界时,擦除为上界类型。
|
|
例如 `List<? extends Number>` → 编译后擦除为 `List<Number>`。
|
|
|
|
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<String>`,`ArrayList<String>` 这个带泛型的父类信息会被保留。
|
|
- 类中成员变量(Field)的泛型类型
|
|
例如 `private List<Integer> scores;`,`List<Integer>` 的泛型信息会被保留。
|
|
- 方法(Method)的参数类型与返回值类型
|
|
例如 `public List<String> getNames(List<String> 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<String>`、`Map<K, V>`)。
|
|
|
|
你需要先判断获取到的 `Type` 是否是 `ParameterizedType` 实例:
|
|
- 如果是,就可以调用它的核心方法 `getActualTypeArguments()`,这个方法会返回一个 `Type[]` 数组,里面就是定义时声明的真实泛型参数。
|
|
- 数组里的每个元素,都对应着原始声明中的泛型参数(如 `List<String>` 会返回一个长度为 1 的数组,元素为 `String`;`Map<K, V>` 会返回长度为 2 的数组,分别为 `K` 和 `V`)。
|
|
|
|
步骤 4:递归解析嵌套泛型(如 `List<List<String>>`)
|
|
如果泛型本身还是一个参数化类型(如 `List<List<String>>`),`getActualTypeArguments()` 返回的 `Type` 仍然是一个 `ParameterizedType`,你可以重复步骤 3,递归解析嵌套的泛型信息。
|
|
|
|
四、不同场景下的获取能力对比
|
|
| 场景 | 示例 | 能否通过反射获取泛型信息? | 原因 |
|
|
|
|
| 类继承的父类泛型 | `class A extends B<String>` | ✅ 能 | 信息保存在类的 `Signature` 属性中 |
|
|
| 类实现的接口泛型 | `class C implements D<Integer>` | ✅ 能 | 信息保存在类的 `Signature` 属性中 |
|
|
| 成员变量的泛型 | `private List<String> list;` | ✅ 能 | 信息保存在字段的 `Signature` 属性中 |
|
|
| 方法返回值的泛型 | `public List<String> getList()` | ✅ 能 | 信息保存在方法的 `Signature` 属性中 |
|
|
| 方法参数的泛型 | `public void setList(List<String> list)` | ✅ 能 | 信息保存在方法的 `Signature` 属性中 |
|
|
| 方法内局部变量的泛型 | `public void test() { List<String> l = new ArrayList<>(); }` | ❌ 不能 | 局部变量的泛型信息编译后完全擦除,无任何保留 |
|
|
| 直接创建的泛型对象 | `new ArrayList<String>()` | ❌ 不能 | 仅在编译期有效,运行时没有任何元数据 |
|
|
|
|
五、局限性与注意事项
|
|
1. 只能获取定义时声明的泛型,无法获取运行时传入的类型
|
|
例如 `List<String> list = new ArrayList<>();`,反射无法知道 `list` 里存的是 `String`,只能知道 `list` 这个变量声明时的类型是 `List<String>`。
|
|
2. 无法获取通配符泛型的具体类型
|
|
例如 `List<? extends Number>`,反射能知道它的上界是 `Number`,但无法知道运行时实际传入的具体子类型。
|
|
3. 类型变量(如 `<T>`)的解析需要上下文
|
|
如果是未绑定的类型变量(如 `class Box<T>` 中的 `T`),反射只能获取到变量名,无法知道它的具体类型,除非它被绑定到了具体的类上(如 `class StringBox extends Box<String>`)。
|
|
|
|
|
|
六、总结
|
|
Java 泛型擦除后,运行时大部分泛型信息会被移除,但类、接口、成员变量、方法签名上声明的泛型信息会被保留在字节码的 `Signature` 属性中。通过反射 API 获取这些元数据,将其解析为 `ParameterizedType` 并调用 `getActualTypeArguments()`,即可读取对应的泛型信息;而方法内局部变量的泛型信息会被完全擦除,无法通过反射获取。
|
|
|