可变参数方法的目的是允许客户端将一个可变数量的参数传递给一个方法,但是这是一个脆弱的抽象,当调用可变数量的参数的时候会在低层创建一个数组来保存可变参数,前文说过,创建数组的时候必须确定类型,这个时候使用泛型或者通配符就会导致编译器警告。例如如下代码:

    1. package item32;
    2. public class VariableParams {
    3. public static <T> void method1(T...strings){
    4. 注: VariableParams.java使用了未经检查或不安全的操作。
    5. 注: 有关详细信息, 请使用 -Xlint:unchecked 重新编译。
    6. Object[] strs = (Object[]) strings;
    7. //会报错
    8. new T[20];
    9. }
    10. public static void main(String[] args) {
    11. method1("a", "b", "c");
    12. }
    13. }

    上述代码为什么可变参数在底层可以生成泛型数组,仅仅是报个警告,而new新建泛型数组的时候会报错答案是,具有泛型或参数化类型的可变参数参数的方法在实践中可能非常有用,因此语言设计人员选择忍受这种不一致。 事实上,Java类库导出了几个这样的方法,包括Arrays.asList(T... a)Collections.addAll(Collection<? super T> c, T... elements)EnumSet.of(E first, E... rest)。 与前面显示的危险方法不同,这些类库方法是类型安全的。

    如果对上述method1添加@SafeVarargs注解,那么会将警告取消,SafeVarargs注解构成了作者对类型安全的方法的承诺。 为了交换这个承诺,编译器同意不要警告用户调用可能不安全的方法。

    下面这段代码很有意思,toArrays()方法会返回可变参数生成的数组。我们调用pickTwo(),在pickTwo中调用toArray生成T类型的数组,然后这段代码会报错。为什么会这样呢?因为toArray方法的可变参数数组会将传入的参数生成一个Object数组,而我们想用一个String[]接收Object[]会报错。

    1. static <T> T[] toArray(T... args) {
    2. //输出:[Ljava.lang.Object;
    3. System.out.println(args.getClass().getName());
    4. T[] s = args;
    5. //输出:[Ljava.lang.Object;
    6. System.out.println(args.getClass().getName());
    7. return s;
    8. }
    9. static <T> T[] pickTwo(T a, T b, T c) {
    10. // 输出:[Ljava.lang.Object;
    11. System.out.println(toArray(a, b, c).getClass().getName());
    12. switch (ThreadLocalRandom.current().nextInt(3)) {
    13. case 0:
    14. return toArray(a, b);
    15. case 1:
    16. return toArray(a, c);
    17. case 2:
    18. return toArray(b, c);
    19. }
    20. throw new AssertionError(); // Can't get here
    21. }
    22. public static void main(String[] args) {
    23. String[] attributes = pickTwo("Good", "Fast", "Cheap");
    24. //改成Object会通过
    25. // Object[] attributes = pickTwo("Good", "Fast", "Cheap");
    26. String[] strings = new String[]{"a", "b", "c"};
    27. System.out.println(strings.getClass().getName());
    28. }

    这个例子是为了让人们认识到给另一个方法访问一个泛型的可变参数数组是不安全的
    **
    这里是安全使用泛型可变参数的典型示例。 此方法将任意数量的列表作为参数,并按顺序返回包含所有输入列表元素的单个列表。 由于该方法使用@SafeVarargs进行标注,因此在声明或其调用站位置上不会生成任何警告:

    1. // Safe method with a generic varargs parameter
    2. @SafeVarargs
    3. static <T> List<T> flatten(List<? extends T>... lists) {
    4. List<T> result = new ArrayList<>();
    5. for (List<? extends T> list : lists)
    6. result.addAll(list);
    7. return result;
    8. }

    请注意,SafeVarargs注解只对不能被重写的方法是合法的,因为不可能保证每个可能的重写方法都是安全的。 在Java 8中,注解仅在静态方法和final实例方法上合法; 在Java 9中,它在私有实例方法中也变为合法。

    使用SafeVarargs注解的替代方法是采用条目 28的建议,并用List参数替换可变参数(这是一个变相的数组)。 下面是应用于我们的flatten方法时,这种方法的样子。 请注意,只有参数声明被更改了:

    1. // List as a typesafe alternative to a generic varargs parameter
    2. static <T> List<T> flatten(List<List<? extends T>> lists) {
    3. List<T> result = new ArrayList<>();
    4. for (List<? extends T> list : lists)
    5. result.addAll(list);
    6. return result;
    7. }

    这种方法的优点是编译器可以证明这种方法是类型安全的。 不必使用SafeVarargs注解来证明其安全性,也不用担心在确定安全性时可能会犯错。 主要缺点是客户端代码有点冗长,运行可能会慢一些。
    这个技巧也可以用在不可能写一个安全的可变参数方法的情况下,就像toArray方法那样。它的列表模拟是List.of方法,所以我们甚至不必编写它; Java类库作者已经为我们完成了这项工作。 pickTwo方法然后变成这样:

    1. static <T> List<T> pickTwo(T a, T b, T c) {
    2. switch(rnd.nextInt(3)) {
    3. case 0: return List.of(a, b);
    4. case 1: return List.of(a, c);
    5. case 2: return List.of(b, c);
    6. }
    7. throw new AssertionError();
    8. }
    9. public static void main(String[] args) {
    10. List<String> attributes = pickTwo("Good", "Fast", "Cheap");
    11. }

    生成的代码是类型安全的,因为它只使用泛型,不是数组。
    总结:可变参数和泛型不能很好地交互,因为可变参数机制是在数组上面构建的脆弱的抽象,并且数组具有与泛型不同的类型规则。 虽然泛型可变参数不是类型安全的,但它们是合法的。 如果选择使用泛型(或参数化)可变参数编写方法,请首先确保该方法是类型安全的,然后使用@SafeVarargs注解对其进行标注,以免造成使用不愉快。