读完这篇,你能彻底搞明白 Java 泛型在编译后究竟变成了什么、javap 里多出来的那个方法是怎么冒出来的,以及遇到泛型相关的诡异报错时该往哪儿查。
第一步:先确认类型擦除擦掉了什么
泛型是编译期概念,编译后类型参数基本被抹平。规则很简单:无界类型变量 T 擦成 Object,有界变量 T extends Number 擦成第一个边界 Number。
List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass()); // true
两者运行时都是 java.util.ArrayList。这也直接解释了下面这些“编译不过”的写法:
if (a instanceof List<String>) { } // 错:无法检查参数化类型
T t = new T(); // 错:擦除后不知道构造哪个类
T[] arr = new T[10]; // 错
List<String>[] arr2 = new List[10]; // 错:泛型数组不允许
注意:泛型类的静态成员不能引用类型参数(static T value; 直接编译错误),因为静态成员属于类本身,而擦除后类的“类型”已经不存在了。
第二步:用 javap 亲眼看擦除结果
写一个最简单的继承场景:
public class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
public class StringBox extends Box<String> {
@Override public void set(String value) { super.set(value); }
@Override public String get() { return super.get(); }
}
编译后执行:
javac Box.java StringBox.java
javap -p -c StringBox
你会看到 StringBox 里居然有 4 个方法:get()、set(String),外加 get()、set(Object) 两个你没写过的方法,并且它们带着 ACC_BRIDGE, ACC_SYNTHETIC 标志。
第三步:理解桥方法为什么必须存在
擦除后父类 Box 的方法是 void set(Object) 和 Object get()。子类写的是 void set(String) 和 String get() —— 参数类型不同,根本不是重写,只是重载。
如果此时调用:
Box<String> box = new StringBox();
box.set("hello"); // 编译期按 Box 的签名调 set(Object)
没有桥方法,StringBox.set(String) 永远不会被调用,多态就断了。所以编译器在子类里偷偷补一个:
// 编译器生成,源码里看不到
public void set(Object value) {
set((String) value); // 里面有 checkcast
}
public Object get() {
return get(); // 返回 String,协变安全,无需强转
}
javap 里那个 checkcast java/lang/String 就是证据。实现泛型接口同理,比如 class MyComp implements Comparable<MyComp>,会自动生成桥方法 compareTo(Object)。
第四步:避开桥方法带来的三个真实坑
坑一:反射拿到一堆多余方法。 getDeclaredMethods() 会把桥方法一起返回。写框架代码、做参数校验、扫描注解时记得过滤:
for (Method m : clazz.getDeclaredMethods()) {
if (m.isBridge() || m.isSynthetic()) continue;
// ...
}
Jackson、Spring 内部都做了这个判断,但你自己写的反射工具很容易漏。
坑二:ClassCastException 报在奇怪的位置。 因为强转发生在桥方法里,堆栈行号可能对不上你写的任何一行代码。看到这类异常,先想“是不是泛型擦除后传错了类型”。
坑三:擦除导致重载冲突。 void f(List<String>) 和 void f(List<Integer>) 无法共存,编译器会报 name clash: same erasure。想区分只能换方法名或加个占位参数。
第五步:需要保留类型信息时怎么办
- 传 Class 令牌:
public <T> T parse(String json, Class<T> type),简单直接。
- 匿名子类 +
getGenericSuperclass():Gson 的 TypeReference、Jackson 的 TypeReference 都是这个套路,因为匿名子类的父类签名在字节码里是保留的(Signature 属性),能拿到 ParameterizedType。
- 泛型数组:用
(T[]) new Object[n] 加 @SuppressWarnings("unchecked"),或 Array.newInstance(clazz, n)。
注意:类型擦除抹掉的是类型参数的使用,不是类/方法上的签名。加了 -g 或默认情况下,Signature 属性仍在 class 文件里,这正是反射能拿到泛型信息的唯一依据。
小结
- 泛型只在编译期存在,运行时
List<String> 和 List<Integer> 是同一个类。
- 擦除规则:无界擦到
Object,有界擦到第一个边界。
- 桥方法是编译器为保住多态而生成的合成方法,带
ACC_BRIDGE + ACC_SYNTHETIC,内部含 checkcast。
- 反射遍历方法时用
isBridge() / isSynthetic() 过滤,别把桥方法当成业务方法。
- 泛型数组、
new T()、instanceof List<String> 都是擦除的直接受害者,改用 Class 令牌或匿名子类方案。