第32条:谨慎并用泛型和可变参数
在Java中,将泛型与可变参数(varargs)结合使用可能导致类型安全问题,需要特别小心处理。
问题本质
可变参数的工作原理
java
// 可变参数实际上是一个数组
void printAll(String... args) {
// 编译器会将可变参数转换为数组
for (String arg : args) {
System.out.println(arg);
}
}
// 调用时可以是:
printAll("a", "b", "c");
printAll(new String[] {"a", "b", "c"});
泛型数组的禁止
之前提到的概念非具体化类型:运行时信息比编译时信息少,这里是因为泛型擦除。
java
// 这是非法的!编译错误
List<String>[] arrayOfLists = new List<String>[10];
// 原因:类型擦除会导致运行时类型检查失效
// List<String> 和 List<Integer> 在运行时都是 List
泛型和可变参数共用时的危险
java
// 危险!可能引发堆污染(Heap Pollution)
@SafeVarargs // 不要轻易添加这个注解!
static <T> void dangerous(List<T>... lists) {
// 这里可以插入类型不安全的代码
Object[] array = lists; // 合法:可变参数实际是数组
List<Integer> intList = List.of(42);
// 这里发生了堆污染!
array[0] = intList; // 编译时无警告,运行时无异常
// 但这里会抛 ClassCastException!
T t = lists[0].get(0); // 试图将 Integer 转换为 T
}
public static void main(String[] args) {
List<String> stringList = List.of("hello");
// 编译通过,但运行时会出问题
dangerous(stringList);
}

所以既然泛型和可变参数共用时是危险的,为什么不和直接泛型数组一样报错,而是报一个警告?
主要原因:Java编译器对泛型和可变参数组合只发警告而非错误,是出于向后兼容和实用性的权衡:Java 5引入泛型时需要保留可变参数API的正常使用,同时考虑到有经验的开发者可以通过安全模式正确使用这个特性,完全禁止会破坏标准库API并迫使开发者采用更不安全的替代方案。
比如:
java
// 很多标准库API将无法实现:
Collections.addAll(Collection<? super T> c, T... elements)
Arrays.asList(T... a)
EnumSet.of(E first, E... rest)
// 开发者会被迫使用:
// 方案1:不使用泛型(回到原始类型)← 更不安全!
// 方案2:使用多个重载方法 ← 笨重
public static <T> List<T> asList(T a)
public static <T> List<T> asList(T a, T b)
public static <T> List<T> asList(T a, T b, T c)
// ...最多到某个有限数量
如何安全使用?
- 添加@SafeVarargs注解
由程序员来保证此方法的安全性,若无法保证安全请勿添加。
- 它没有在可变参数数组中保存任何值
- 它没有对不被信任的代码开放该数组(或者其克隆程序)。
java
// 安全的情况:不破坏类型安全的模式
@SafeVarargs
static <T> List<T> flatten(List<? extends T>... lists) {
List<T> result = new ArrayList<>();
for (List<? extends T> list : lists) {
result.addAll(list);
}
return result;
}
// 安全条件:
// 1. 不对可变参数数组进行修改
// 2. 不将数组暴露给不受信任的代码
// 3. 不将泛型数组作为返回值
- 使用List接收参数
java
// 更安全的替代方案:使用List
static <T> List<T> flatten(List<List<? extends T>> lists) {
List<T> result = new ArrayList<>();
for (List<? extends T> list : lists) {
result.addAll(list);
}
return result;
}
// 使用
List<String> flat = flatten(List.of(
List.of("a", "b"),
List.of("c", "d")
));
使用示例
安全使用场景
java
// 场景1:只是传递参数,不做修改
@SafeVarargs
static <T> void printAll(T... items) {
for (T item : items) {
System.out.println(item);
}
}
// 场景2:用于工厂方法
@SafeVarargs
static <E extends Enum<E>> EnumSet<E> of(E first, E... rest) {
EnumSet<E> result = EnumSet.of(first);
for (E e : rest) {
result.add(e);
}
return result;
}
允许另一个方法访问一个泛型可变参数数组是不安全的
书上的一个例子:
java
class SafePicker {
private static final Random rnd = new Random();
// 对比:危险的数组版本
static <T> T[] pickTwoUnsafe(T a, T b, T c) {
switch (rnd.nextInt(3)) {
case 0: return toArray(a, b); // ❌ 危险!
case 1: return toArray(a, c); // ❌
case 2: return toArray(b, c); // ❌
default: throw new AssertionError();
}
}
// 危险的辅助方法
static <T> T[] toArray(T... args) {
return args; // ❌ 返回泛型数组
}
public static void main(String[] args) {
// 运行时报错:class [Ljava.lang.Object; cannot be cast to class [Ljava.lang.String;
String[] two2 = pickTwoUnsafe("张三", "李四", "王五");
System.out.println(Arrays.toString(two2));
}
}

很显然,这是不对的。
我们简化一下:
java
class SafePicker {
// 对比:危险的数组版本
static <T> T[] pickTwoUnsafe(T a, T b, T c) {
return toArray(a, b, c);
}
// 危险的辅助方法
static <T> T[] toArray(T... args) {
return args;
}
public static void main(String[] args) {
// 正常运行 ✅
Integer[] integers = toArray(1, 2);
System.out.println(Arrays.toString(integers));
// 报错❌
Integer[] integers1 = pickTwoUnsafe(1, 2, 3);
System.out.println(Arrays.toString(integers1));
}
}

为什么直接调用toArray是正常的,而通过pickTwoUnsafe再调用toArray就会报错呢?
可变参数其实是创建的泛型数组传入。
当直接调用时:
java
Integer[] arr = toArray(1, 2);

当泛型擦除后,
- 看到调用点:toArray(1, 2),推断 T = Integer
- 生成具体的Integer类型数组传入toArray
- 返回便是正确的。
当间接调用时:
java
Integer[] integers1 = pickTwoUnsafe(1, 2, 3);
static <T> T[] pickTwoUnsafe(T a, T b, T c) {
return toArray(a, b, c);
}


- 看到 toArray(a, b, c) 调用
- T 是类型参数,已被擦除为 Object
- 只能生成通用Object类型数组传入toArray
- 那么返回时你用Integer数组接收肯定就报错了。
当然可能还会有疑问,为什么在内部无法推断出T的类型呢?
可变参数数组是在运行时创建,但创建的类型在编译时决定。Java 的泛型不支持"类型传递(Type Reification)"到方法内部,每个方法的编译相互独立,当编译pickTwoUnsafe中的toArray时,编译器只知道传入是T,返回是T,那就只能确定为Object数组了。
编译期事件顺序:
- 类型检查(使用泛型信息)
- 泛型擦除(移除类型参数)
- 确定数组创建类型(基于擦除后的类型)
- 生成字节码(包含具体的anewarray指令)
使用安全的List.of()等方法
java
// 在 Java 标准库中,类似这样实现:
static <E> List<E> of(E e1, E e2) {
return new ImmutableCollections.List2<>(e1, e2);
}
使用 List.of() 完全避免了泛型数组的创建。
编译时就能保证类型安全,不会在运行时抛出 ClassCastException。
总结
其实归根结底,就是可变参数 + 泛型这种不安全的使用导致的。可变参数和泛型不能良好地合作,这是因为可变参数设施是构建在顶级数组之上的一个技术露底,泛型数组有不同的类型规则。虽然泛型可变参数不是类型安全的,但它们是合法的。如果选择编写带有泛型(或者参数化)可变参数的方法,首先要确保该方法是类型安全的,然后用@SafeVarargs对它进行注解,这样使用起来就不会出现不愉快的情况了。