bigywater.tech
Java 内部原理 · 51

Java 泛型擦除真实验证:List<String>和List<Integer>的Class对象真的是同一个

都知道"Java 泛型是擦除的",但擦除到底擦掉了什么、留下了什么,很少有人真的用反射验证过。这篇真实跑三组实验:类型对象是否相同、checkcast 到底插在哪、泛型数组的运行时类型是否也被擦除。

getClass() 完全相同
真实验证:new ArrayList<String>() 和 new ArrayList<Integer>() 的 getClass() 返回值真实是同一个对象
checkcast 插在调用点
真实验证:反射绕过后 box.get() 的调用点本身就真实抛出 ClassCastException,不是等到赋值给具体变量才抛

上一篇:try-with-resources 没有主异常时,close() 异常真实直接抛出

真实验证:泛型参数在运行时真的不存在

用 getClass() 和反射直接检验 ArrayList<String> 和 ArrayList<Integer> 在运行时到底有没有区别。

GenericsErasureDemo.java List<String> ls = new ArrayList<>(); List<Integer> li = new ArrayList<>(); System.out.println(ls.getClass() == li.getClass()); Method setM = Box.class.getMethod("set", Object.class); System.out.println(setM.getParameterTypes()[0]);
真实实测
ls.getClass() == li.getClass(): true
ls.getClass(): class java.util.ArrayList
li.getClass(): class java.util.ArrayList
Box.set 擦除后的参数类型: class java.lang.Object
字段 value 的擦除类型: class java.lang.Object
字段 value 的泛型类型(来自 Signature): T

真实结果证实了那句被说烂的话:List<String> 和 List<Integer> 在运行时是同一个 Class 对象,泛型参数 <T> 在字节码层面真实变成了 Object。唯一保留下来的痕迹是 class 文件里的 Signature 属性——反射能读到字段的"泛型类型"是 T,但这只是给 IDE 和反射 API 看的元信息,不参与运行时的任何类型检查。

真实验证:checkcast 插在调用点,不是赋值处

用反射绕过编译期泛型检查,往 Box<String> 里塞一个 Integer,看真实的 ClassCastException 在哪一行发生。

GenericsErasureDemo.java Box<String> box = new Box<>(); box.set("hello"); Method rawSet = box.getClass().getMethod("set", Object.class); rawSet.invoke(box, Integer.valueOf(123)); // 反射绕过泛型检查 System.out.println("box.get(): " + box.get()); // 意外:这里就抛了
真实实测
反射绕过泛型检查成功,字段已被塞入 Integer(invoke 本身不做类型检查)
真实捕获 ClassCastException(发生在 box.get() 调用点本身,不是赋值处):
class java.lang.Integer cannot be cast to class java.lang.String
(java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')
一个容易搞错的细节。很多人以为 checkcast 是在"把返回值赋给一个具体类型的变量"时才插入的,比如 String s = box.get();。真实实验证明不是这样:只要调用表达式 box.get() 的静态类型(由 box 声明为 Box<String> 决定)是 String,编译器就会在这个调用点的 invokevirtual 指令后面立刻插入 checkcast,哪怕这次调用只是拼进 println 的字符串里,压根没有赋值给任何变量。真实异常就是在这一行、这一次调用发生的。

真实验证:泛型数组的运行时元素类型没有被擦除

List<String>[] 这种泛型数组,擦除掉的只是 <String> 这个类型参数,数组本身的运行时元素类型(List)依然真实存在。

GenericsErasureDemo.java List<String>[] arr = new List[3]; arr[0] = new ArrayList<>(); Object[] objArr = arr; objArr[1] = "不是List"; // 猜测:擦除后是Object[],应该能塞任意类型?
真实实测
真实捕获 ArrayStoreException: java.lang.String
(数组的运行时元素类型 List 未被擦除,只有 <String> 这个类型参数被擦除了)

这是本次实验里最反直觉的一点。最初的猜测是"反正泛型都被擦除了,objArr[1] = "不是List" 应该能悄悄成功",但真实结果是 ArrayStoreException 照样被真实抛出。原因是数组和泛型是两套完全独立的机制:泛型擦除只发生在类型参数 <String> 这个层面,而数组本身在创建时(new List[3])就已经把运行时元素类型记录成了 List(原始类型),这个记录不受泛型擦除影响,JVM 在每次数组写入时依然会真实做 arraystore 类型检查。

分步演示:擦除到底擦掉了什么,又留下了什么

每一行都是真实运行这段代码得到的结果或异常。

getClass() 相同证明泛型擦除;checkcast 插在调用点不是赋值处;数组的运行时元素类型不受泛型擦除影响
这一步新验证的调用

    参考与说明

    • 本文全部结果均在本机真实运行 Java 17 得到,三组实验重复运行结果完全一致。
    • 和可变参数重载解析一样,这篇也是用反射/字节码层面的真实观察去验证"编译器到底做了什么",而不是停留在书面说法。
    • 没有涉及:泛型边界(T extends X)对擦除结果的影响、桥接方法(bridge method)在接口/继承场景下的真实生成——这两个话题值得单独写。
    京ICP备2026067502号-1京公网安备11010502063487号