T... args 这种泛型可变参数写法编译时经常只给一个"unchecked"警告,很多人习惯性忽略。真实构造一次经典场景,看编译器放过去的这个口子到底能捅出多大的篓子。
上一篇:Collections.unmodifiableList 真实只是一层视图:改原始 list,不可变的那份也真实跟着变
泛型信息在运行时会被擦除,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[] 使用才没暴露出来。
调用方完全信任方法签名,把返回值强转成 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')
每一行都是真实运行这段代码得到的结果,编译器的 unchecked 警告没能拦下任何一步。