2364 字
约 7 分钟
20
S04-X04 · 泛型擦除与桥方法:为什么运行时拿不到 T

泛型是编译期的类型检查协议,不是运行时的类型系统。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. 自测

  1. List<String> 与 List<Integer> 的 getClass() 为什么必然相等?这直接否定了哪两种写法?
  2. new T[10] 被禁止的根因,是编译器保守,还是 JVM 数组语义的要求?
  3. 报错栈顶出现 compareTo(Object) 而你只写了 compareTo(Version),那一帧是谁生成的,它做了什么?
  4. 为什么 TypeRef<List<String>>() 不加 {} 就拿不到泛型实参?
  5. 堆污染为什么在写入点不报错,只在读出点抛 ClassCastException?
  6. @SafeVarargs 加了之后,运行时多做了哪一道检查?
S04-X04 · 泛型擦除与桥方法:为什么运行时拿不到 T
https://www.clxhxhhr.top/posts/5648/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。