都知道"Java 泛型是擦除的",但擦除到底擦掉了什么、留下了什么,很少有人真的用反射验证过。这篇真实跑三组实验:类型对象是否相同、checkcast 到底插在哪、泛型数组的运行时类型是否也被擦除。
上一篇: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 看的元信息,不参与运行时的任何类型检查。
用反射绕过编译期泛型检查,往 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')
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 类型检查。
每一行都是真实运行这段代码得到的结果或异常。