泛型是编译期的类型检查协议,不是运行时的类型系统。javac 擦掉类型参数,在需要处插入强制转换;为让多态在擦除后仍成立,再合成桥方法。这两步决定了三件常被误判的事:ClassCastException 为什么抛在读出点、instanceof List<String> 为什么非法、@SafeVarargs 到底在担保什么。
1. 擦除规则:只有两种落点
| 源码形态 | 擦除结果 | 运行时可见 |
|---|---|---|
List<T>(无界) |
List,元素按 Object 存取 |
无类型参数 |
List<T extends Number> |
List,元素按 Number 存取 |
上界进字节码 |
Map<String, List<Integer>> |
Map |
仅 Signature 属性保留声明 |
T[](无界 / 有界 Number) |
Object[] / Number[] |
组件类型是边界 |
public class Box<T extends Number> {
private T value;
public void set(T v) { this.value = v; }
public T get() { return value; }
public double twice() { return value.doubleValue() * 2; }
}
擦除后等价于 Box { private Number value; public void set(Number); public Number get(); },set 的 descriptor 是 (Ljava/lang/Number;)V。注意 twice() 能直接调 value.doubleValue()——擦除把 T 换成了 Number。若 T 无界,这行得写成 ((Number) value).doubleValue(),或者根本编译不过:Object 上没有这个方法。字段泛型只留在 Signature 属性里(Signature: TT;),供反射读取,不参与链接与分派。
2. 同一个类:List<String> 与 List<Integer>
List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass()); // true
System.out.println(a.getClass().getName()); // java.util.ArrayList
java.util.ArrayList<String> 在运行时不存在。三条硬限制随之而来:无法从实例反推元素类型;重载不能只差泛型实参(f(List<String>) 与 f(List<Integer>) 报 both methods have same erasure);静态成员不能引用类类型参数。
| 写法 | 编译器反应 | 原因 |
|---|---|---|
new T() |
type parameter T cannot be instantiated |
没有具体构造器可调 |
new T[10] |
generic array creation |
数组需要运行时可确定的组件类型 |
instanceof List<String> |
illegal generic type for instanceof |
不可具体化 |
catch (T e) |
不允许 | 异常类型必须可具体化 |
T.class |
不允许 | 没有对应的 Class 对象 |
static T field |
不允许 | 静态上下文无类型参数 |
3. 为什么不能 new T[] 与 new T()
new T() 卡在构造器上:擦除后 T 只剩上界,而 new Number() 未必合法(抽象类、可能没有无参构造)。两条出路:
T a = clazz.getDeclaredConstructor().newInstance(); // 传 Class
T b = supplier.get(); // 传工厂,不抛受检异常
new T[] 卡在数组语义上。anewarray 需要一个具体的组件类,擦除后编译器只能生成 Object[];而 Object[] 不能冒充 T[]——若 T 是 String,Object[] 里塞进 Integer 就破坏了数组的运行时类型安全。所以编译器直接拒绝,而非悄悄降级。三种绕过代价不同:
@SuppressWarnings("unchecked")
T[] a = (T[]) new Object[10]; // 能编译,真实类型仍是 Object[]
T[] b = (T[]) Array.newInstance(clazz, 10); // 组件类型正确,需 clazz
Object[] c = new Object[10]; // 不假装自己是 T[]
第一种最阴:T = String 时 String[] s = (String[]) a; 运行时抛 CCE,肇事点却是构造 a 那行。ArrayList 用 Object[] elementData 加单元素 (E) 转换,回避整数组强转。
4. 桥方法:擦除后多态靠什么站住
Comparable<T> 声明 int compareTo(T o),擦除后是 compareTo(Object)。子类写 class Version implements Comparable<Version> 并实现 compareTo(Version),方法表里只有 compareTo(LVersion;)I;用父类型引用调 compareTo(Object) 时按 descriptor 查找会落空。javac 的解法是合成一个桥:在子类里额外生成 compareTo(Object),方法体只有一行——把参数强转成 Version 再转发。javap -p -c 的典型输出形态:
class Version implements java.lang.Comparable<Version> {
public int compareTo(Version); // descriptor: (LVersion;)I
flags: ACC_PUBLIC
2: invokevirtual #7 // Method compareTo:(LVersion;)I
5: ireturn
public int compareTo(java.lang.Object); // 桥:(Ljava/lang/Object;)I
flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
2: checkcast #9 // class Version
5: invokevirtual #7 // Method compareTo:(LVersion;)I
8: ireturn
}
| 标志/属性 | 含义 | 排错用途 |
|---|---|---|
ACC_BRIDGE |
合成的转发方法 | 判断"多出来的方法"是桥 |
ACC_SYNTHETIC |
源码不存在,javac 生成 | 反射遍历应跳过 |
ACC_VARARGS |
变长参数 | 与桥叠加时签名更易混 |
Signature 属性 |
保留泛型声明 | 反射读实参的唯一来源 |
三个可观测现象:getDeclaredMethods() 数量比源码多,isBridge() 为 true;getMethod("compareTo", Object.class) 拿到桥,传错类型时 CCE 抛在桥内部,栈顶是你没写过的名字——这正是"报错指向幻影方法"的来源;框架解析泛型必须过滤 bridge,否则拿到 Object 形参丢掉特化信息。
for (Method m : Version.class.getDeclaredMethods()) {
if (m.isBridge() || m.isSynthetic()) continue; // 跳过合成方法
System.out.println(m.toGenericString());
}
5. 堆污染与 @SafeVarargs
T... 擦除后是 Object[](有界时是边界数组)。编译器对泛型数组创建一律给 unchecked 警告,因为数组协变、泛型不变,两者一叠加就能被绕过。最小可复现的堆污染(JLS 经典例子):
@SuppressWarnings("unchecked")
List<String>[] lsa = new List[1]; // 擦除后是 List[],协变允许
Object[] oa = lsa; // List[] 是 Object[]
oa[0] = List.of(1); // 塞进 List<Integer>,此处无异常
String s = lsa[0].get(0); // checkcast String 在这里抛 CCE
写入点安静,读出点爆炸。这就是"擦除把类型错误从写入推迟到读出"的完整机制。
@SafeVarargs 是给编译器的担保书:方法不泄露 varargs 数组,也不写入不符元素。JDK 17 的适用条件:必须是 static、final、private 方法或构造器(非 final 实例方法不能标,子类可覆盖并破坏担保);不把 T... 转成 T[] 返回;不写入非 T 元素。标注后调用点不再有 unchecked 警告,但没有增加任何运行时检查——担保靠人。
6. 拿泛型参数的唯一途径:ParameterizedType
关键区分:擦除擦的是实例,不擦声明。类文件里字段、方法形参与返回、父类、接口的泛型信息完整保留在 Signature 属性中,所以只能从声明位置读到 List<String>。
public abstract class TypeRef<T> {
private final Type type;
protected TypeRef() {
Type superclass = getClass().getGenericSuperclass();
this.type = ((ParameterizedType) superclass).getActualTypeArguments()[0];
}
}
TypeRef<List<String>> ref = new TypeRef<List<String>>() {}; // 花括号必须有
System.out.println(ref.type()); // java.util.List<java.lang.String>
花括号是全部机关:不带 {} 时 getClass() 返回 TypeRef,getGenericSuperclass() 得到 Object,强转立刻 CCE。带上 {} 后 javac 生成 TypeRef$1 extends TypeRef<List<String>>,其父类 Signature 写着 Ljava/util/List<Ljava/lang/String;>;,才返回真正的 ParameterizedType。Jackson 的 TypeReference 与 Spring 的 ParameterizedTypeReference 都是这个套路。
Type 子接口 |
代表形态 | 出现在哪 |
|---|---|---|
Class |
String |
非泛型类型 |
ParameterizedType |
List<String> |
带实参的声明 |
GenericArrayType |
T[]、List<String>[] |
组件是变量或参数化类型 |
TypeVariable |
T |
声明位置的类型变量 |
WildcardType |
? extends Number |
通配符实参 |
7. PECS 与擦除的关系
? extends / ? super 的读写限制全部由 javac 在编译期施加,运行时不存在 List<Number> 这个类型,也就没有运行时校验。
| 声明 | 擦除后 | 读出来是 | 能写入 |
|---|---|---|---|
List<Number> |
List |
Number |
Number |
List<? extends Number> |
List |
Number |
只能 null |
List<? super Integer> |
List |
Object |
Integer 及子类 |
List(原始类型) |
List |
Object |
任何东西 |
src.add(1) 对 List<? extends Number> 是编译错误(除 null 外不可写);dst.add(1) 对 List<? super Integer> 合法,但 dst.get(0) 只能读成 Object。原始类型是逃生口:((List) src).add("x") 只产生 unchecked 警告,堆里就真混进了 String,下一次 for (Number x : src) 抛 CCE。PECS 把这类错误挡在编译期;一旦被原始类型破口,运行时没有任何东西能兜住。
8. 排错
| 现象 | 真实原因 | 处理 |
|---|---|---|
| 栈顶帧是自己没写过的方法 | 桥方法内 checkcast 失败 |
看桥的下一帧,那才是肇事实现 |
CCE 抛在 for 循环或赋值那行 |
上游用原始类型写入了错元素 | -Xlint:unchecked 找写入点,按读出点回溯 |
both methods have same erasure |
重载只差泛型实参 | 改方法名,或增加不同形态的参数 |
getGenericSuperclass() 返回 Object |
少写了 {} |
用匿名子类实例化 TypeRef |
(T[]) new Object[n] 后续 CCE |
整数组强转伪造了组件类型 | 改 Array.newInstance(clazz, n) |
9. 边界
| 需求 | 该用的手段 | 说明 |
|---|---|---|
| 运行时需要元素类型 | 显式传 Class<E> |
泛型本身不提供 |
需要 T[] |
Array.newInstance(clazz, n) |
组件类型必须具体 |
| 需要读泛型实参 | TypeRef + ParameterizedType |
只能读声明位置 |
| 需要按类型分派 | Map<Class<?>, Handler> |
擦除让重载失效 |
| 可靠去重/比较 | 显式 Comparator 或 equals |
不依赖泛型 |
10. 自测
List<String>与List<Integer>的getClass()为什么必然相等?这直接否定了哪两种写法?new T[10]被禁止的根因,是编译器保守,还是 JVM 数组语义的要求?- 报错栈顶出现
compareTo(Object)而你只写了compareTo(Version),那一帧是谁生成的,它做了什么? - 为什么
TypeRef<List<String>>()不加{}就拿不到泛型实参? - 堆污染为什么在写入点不报错,只在读出点抛
ClassCastException? @SafeVarargs加了之后,运行时多做了哪一道检查?