Java 泛型擦除(Type Erasure)
(含匿名内部类保留泛型信息的特殊情况)
一、核心定义
泛型擦除是 Java 为兼容 1.5 前非泛型代码的编译期行为:
编译时移除泛型参数信息,将泛型类型替换为“原始类型”,运行时不存在 List<String>
/List<Integer>
区别,仅保留 List.class
。
→ 本质:泛型是“编译期语法糖”,运行时无泛型类型实例。
二、3类核心擦除规则
泛型场景 | 擦除规则 | 示例 | 擦除后结果(编译期) |
---|---|---|---|
类/接口泛型 | 无边界→Object ;有边界→边界类型 |
class MyList<T> |
class MyList (T→Object) |
class NumList<T extends Number> |
class NumList (T→Number) |
||
方法泛型(参数/返回值) | 按类泛型规则替换,同时插入类型转换代码 | public <T> T get(T obj) |
public Object get(Object obj) |
泛型通配符 |
? →Object ;? extends A →A ;? super A →Object
|
List<? extends Number> |
List<Number> |
编译期:编译器处理泛型代码时,会按规则擦除泛型信息(比如 List<String> 擦成 List、? extends Number 擦成 Number 等 ),同时插入必要的类型转换代码,保证编译时的类型检查能通过。
运行期:JVM 加载的字节码中,泛型的具体参数(如 String、Integer )已被擦除,只剩原始类型(如 Object、Number 对应的逻辑 ),运行时无法区分 List<String> 和 List<Integer> 的泛型差异。
泛型声明(<>及内部内容)被彻底删除,只剩原始类型List;
代码中使用泛型参数的地方(如元素类型)被替换为边界类型Number(保证字节码的类型兼容性)。
三、泛型信息的保留机制
擦除仅移除“运行时动态类型”,但部分场景会保留泛型元数据(存于字节码“签名”属性,供反射读取):
1. 常规保留场景(可反射获取)(声明处保留场景)
- 类声明:
class MyList<T>
(用getTypeParameters()
获取) - 方法参数:
void func(List<String> list)
(用getGenericParameterTypes()
获取) - 方法返回值:
List<Integer> getList()
(用getGenericReturnType()
获取) - 字段声明:
private Map<String, Integer> map
(用getGenericType()
获取)
2. 特殊情况:匿名内部类的泛型保留
匿名内部类在实例化时若指定泛型参数,编译器会将泛型信息保留在类的元数据中(这是唯一能在“非声明处”保留泛型的场景)。
示例代码:
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
public class AnonymousGenericExample {
// 这是一个普通的方法,不是泛型方法声明
public void processList() {
// 这里创建了一个匿名内部类(注意末尾的{})
// 这个泛型参数<String>是在非声明处指定的
ArrayList anonymousList = new ArrayList<String>() {};
// 通过反射获取匿名内部类的泛型信息
try {
// 获取当前类中所有字段(虽然这里是局部变量,但原理相同)
Type type = anonymousList.getClass().getGenericSuperclass();
if (type instanceof ParameterizedType) {
ParameterizedType paramType = (ParameterizedType) type;
Type[] actualTypeArgs = paramType.getActualTypeArguments();
System.out.println("匿名内部类的泛型参数: " + actualTypeArgs[0]);
// 输出: 匿名内部类的泛型参数: class java.lang.String
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new AnonymousGenericExample().processList();
}
}
原理:
匿名内部类会生成独立的 .class
文件(如 AnonymousGeneric$1.class
),编译器会将泛型参数(如 String
)写入该类的元数据,因此反射可获取。
3. 不保留的泛型(反射也拿不到)
- 局部变量:
method内 List<String> list = new ArrayList<>()
(非匿名内部类) - 普通实例化:
new ArrayList<String>()
(非匿名内部类,仅编译期检查)
四、实用结论
- 运行时
list.getClass()
只能拿到原始类型(如ArrayList.class
),无法区分泛型参数; - 匿名内部类是特殊例外,其泛型参数会被保留在元数据中,可通过反射获取;
- 反射能获取的泛型信息,本质是“类结构中声明的泛型契约”,而非运行时动态类型;
- 泛型擦除后,编译器会自动插入类型转换代码,保证运行时类型安全(如
String s = list.get(0)
)。