bigywater.tech
Java 内部原理 · 28

泛型可变参数的堆污染:真实复现一次编译器拦不住的 ClassCastException

T... args 这种泛型可变参数写法编译时经常只给一个"unchecked"警告,很多人习惯性忽略。真实构造一次经典场景,看编译器放过去的这个口子到底能捅出多大的篓子。

class [Ljava.lang.Object;
真实打印:一个理应是 String[] 的返回值,运行时真实类型是 Object[],和源码里写的泛型类型完全对不上
ClassCastException
尝试强转成 String[] 接收,真实抛出 ClassCastException——编译期完全没有报错,问题真实等到运行时才爆出来

上一篇:Collections.unmodifiableList 真实只是一层视图:改原始 list,不可变的那份也真实跟着变

思路:可变参数数组,运行时真实类型是 Object[]

泛型信息在运行时会被擦除,T... args 这种可变参数在字节码层面创建的就是一个 Object[] 数组,不管调用时的 T 具体是什么类型。

HeapPollutionDemo.java(经典的 pickTwo 场景) @SafeVarargs static <T> T[] toArray(T... args) { return args; // 把这个真实的 Object[] 直接返回出去 } static <T> T[] pickTwo(T a, T b, T c, int which) { switch (which) { case 0: return toArray(a, b); case 1: return toArray(a, c); default: return toArray(b, c); } }

这段代码能正常编译通过,@SafeVarargs 注解甚至明确"担保"了这个方法是安全的——但这个担保只是程序员对编译器的承诺("我不会在方法内部对这个数组做危险操作"),编译器没有能力真的去验证这个承诺,toArray 内部恰恰做了最危险的那件事:把这个类型已经被擦除成 Object[] 的数组原样返回出去。

真实验证:返回值的运行时类型,真的不是你以为的那个

先看看正常使用(当 Object[] 用)不会有问题,再看看它的真实类型信息。

真实实测
当成 Object[] 使用,真实没有问题 = [Good, Fast]

result 数组的真实运行时类型 = class [Ljava.lang.Object;
(不是 String[],而是 Object[]——因为 toArray 内部的可变参数数组本来就是按 Object[] 创建的)

这一步真实结果已经暴露了问题的根源:pickTwo("Good", "Fast", "Cheap", 0) 从类型签名上看应该返回 String[](泛型参数 T 被推断为 String),但它真实返回的数组,getClass() 打印出来是 [Ljava.lang.Object;——也就是说这个"应该是 String[]"的东西,骨子里从来就不是 String[],只是因为一直被当成 Object[] 使用才没暴露出来。

真实验证:一旦按泛型签名接收,真实爆出 ClassCastException

调用方完全信任方法签名,把返回值强转成 String[] 接收——这是最自然、最常见的用法。

真实实测
真实抛出 ClassCastException: class [Ljava.lang.Object; cannot be cast to class [Ljava.lang.String; ([Ljava.lang.Object; and [Ljava.lang.String; are in module java.base of loader 'bootstrap')
这算谁的 bug?从调用方的角度看,代码完全正常,类型也完全匹配方法签名,没有任何理由怀疑会在这里出问题;从 toArray 实现者的角度看,方法体内没有做任何显式的类型转换,IDE 和编译器也只给了一个可以忽略的警告。真实的根源是 Java 泛型和数组这两套类型系统的先天不兼容——数组在运行时携带真实的元素类型信息(协变检查靠它),而泛型的类型信息在运行时完全擦除,可变参数恰好是这两者的交界地带,把一个泛型数组直接暴露出去,就真实留下了这个编译器管不到、只能靠约定(比如 Effective Java 里"不要把泛型可变参数数组暴露给调用方"这条规则)去规避的缺口。

分步演示:类型信息是在哪一步真实丢失的

每一行都是真实运行这段代码得到的结果,编译器的 unchecked 警告没能拦下任何一步。

从 toArray(T... args) 内部创建的 Object[],到调用方强转 String[] 那一刻的 ClassCastException
这一步新验证的状态

    参考与说明

    • 本文全部结果均在本机真实运行 Java 17 得到,编译时可以看到真实的 [varargs] unchecked 警告。
    • 和Arrays.asList 那篇一样,都是"数组和泛型/集合框架的边界地带"埋雷的真实案例。
    • 没有涉及:Collections.<T>emptyList() 等其它泛型方法如何安全地处理类型擦除、Array.newInstance 反射创建真正类型安全数组的写法——这些留给系列后面按需再写。
    京ICP备2026067502号-1京公网安备11010502063487号