Effective Java 第三版——32.合理地结合泛型和可变参数 (2)

编译此方法时,编译器会生成代码以创建一个将两个T实例传递给toArray的可变参数数组。 这段代码分配了一个Object []类型的数组,它是保证保存这些实例的最具体的类型,而不管在调用位置传递给pickTwo的对象是什么类型。 toArray方法只是简单地将这个数组返回给pickTwo,然后pickTwo将它返回给调用者,所以pickTwo总是返回一个Object []类型的数组。

现在考虑这个测试pickTw的main方法:

public static void main(String[] args) { String[] attributes = pickTwo("Good", "Fast", "Cheap"); }

这种方法没有任何问题,因此它编译时不会产生任何警告。 但是当运行它时,抛出一个ClassCastException异常,尽管不包含可见的转换。 你没有看到的是,编译器已经生成了一个隐藏的强制转换为由pickTwo返回的值的String []类型,以便它可以存储在属性中。 转换失败,因为Object []不是String []的子类型。 这种故障相当令人不安,因为它从实际导致堆污染(toArray)的方法中移除了两个级别,并且在实际参数存储在其中之后,可变参数数组未被修改。

这个例子是为了让人们认识到给另一个方法访问一个泛型的可变参数数组是不安全的,除了两个例外:将数组传递给另一个可变参数方法是安全的,这个方法是用@SafeVarargs正确标注的, 将数组传递给一个非可变参数的方法是安全的,该方法仅计算数组内容的一些方法。

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

// Safe method with a generic varargs parameter @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; }

决定何时使用SafeVarargs注解的规则很简单:在每种方法上使用@SafeVarargs,并使用泛型或参数化类型的可变参数,这样用户就不会因不必要的和令人困惑的编译器警告而担忧。 这意味着你不应该写危险或者toArray等不安全的可变参数方法。 每次编译器警告你可能会受到来自你控制的方法中泛型可变参数的堆污染时,请检查该方法是否安全。 提醒一下,在下列情况下,泛型可变参数方法是安全的:
1.它不会在可变参数数组中存储任何东西

2.它不会使数组(或克隆)对不可信代码可见。 如果违反这些禁令中的任何一项,请修复。

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

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

// List as a typesafe alternative to a generic varargs parameter 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.of结合使用,以允许可变数量的参数。 请注意,这种方法依赖于List.of声明使用@SafeVarargs注解:
audience = flatten(List.of(friends, romans, countrymen));

这种方法的优点是编译器可以证明这种方法是类型安全的。 不必使用SafeVarargs注解来证明其安全性,也不用担心在确定安全性时可能会犯错。 主要缺点是客户端代码有点冗长,运行可能会慢一些。

这个技巧也可以用在不可能写一个安全的可变参数方法的情况下,就像第147页的toArray方法那样。它的列表模拟是List.of方法,所以我们甚至不必编写它; Java类库作者已经为我们完成了这项工作。 pickTwo方法然后变成这样:

static <T> List<T> pickTwo(T a, T b, T c) { switch(rnd.nextInt(3)) { case 0: return List.of(a, b); case 1: return List.of(a, c); case 2: return List.of(b, c); } throw new AssertionError(); }

main方变成这样:

public static void main(String[] args) { List<String> attributes = pickTwo("Good", "Fast", "Cheap"); }

生成的代码是类型安全的,因为它只使用泛型,不是数组。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpsfgd.html