Tips
《Effective Java, Third Edition》一书英文版已经出版,这本书的第二版想必不少人都读过,号称Java四大名著之一,不过第二版2009年出版,到如今已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深入的变化。
在这里第一时间翻译成中文版。供你们学习分享之用。数组
在Java 5中,可变参数方法(条目 53)和泛型都被添加到平台中,因此你可能但愿它们可以正常交互; 可悲的是,他们并无。 可变参数的目的是容许客户端将一个可变数量的参数传递给一个方法,但这是一个脆弱的抽象( leaky abstraction):当你调用一个可变参数方法时,会建立一个数组来保存可变参数;那个应该是实现细节的数组是可见的。 所以,当可变参数具备泛型或参数化类型时,会致使编译器警告混淆。安全
回顾条目 28,非具体化( non-reifiable)的类型是其运行时表示比其编译时表示具备更少信息的类型,而且几乎全部泛型和参数化类型都是不可具体化的。 若是某个方法声明其可变参数为非具体化的类型,则编译器将在该声明上生成警告。 若是在推断类型不可肯定的可变参数参数上调用该方法,那么编译器也会在调用中生成警告。 警告看起来像这样:dom
warning: [unchecked] Possible heap pollution from parameterized vararg type List<String>
当参数化类型的变量引用不属于该类型的对象时会发生堆污染(Heap pollution)[JLS,4.12.2]。 它会致使编译器的自动生成的强制转换失败,违反了泛型类型系统的基本保证。工具
例如,请考虑如下方法,该方法是第127页上的代码片断的一个不太明显的变体:学习
// Mixing generics and varargs can violate type safety! static void dangerous(List<String>... stringLists) { List<Integer> intList = List.of(42); Object[] objects = stringLists; objects[0] = intList; // Heap pollution String s = stringLists[0].get(0); // ClassCastException }
此方法没有可见的强制转换,但在调用一个或多个参数时抛出ClassCastException异常。 它的最后一行有一个由编译器生成的隐形转换。 这种转换失败,代表类型安全性已经被破坏,而且将值保存在泛型可变参数数组参数中是不安全的。测试
这个例子引起了一个有趣的问题:为何声明一个带有泛型可变参数的方法是合法的,当明确建立一个泛型数组是非法的时候呢? 换句话说,为何前面显示的方法只生成一个警告,而127页上的代码片断会生成一个错误? 答案是,具备泛型或参数化类型的可变参数参数的方法在实践中可能很是有用,所以语言设计人员选择忍受这种不一致。 事实上,Java类库导出了几个这样的方法,包括Arrays.asList(T... a)
,Collections.addAll(Collection<? super T> c, T... elements)
,EnumSet.of(E first, E... rest)
。 与前面显示的危险方法不一样,这些类库方法是类型安全的。翻译
在Java 7中,SafeVarargs
注解已添加到平台,以容许具备泛型可变参数的方法的做者自动禁止客户端警告。 实质上,SafeVarargs
注解构成了做者对类型安全的方法的承诺。 为了交换这个承诺,编译器赞成不要警告用户调用可能不安全的方法。设计
除非它其实是安全的,不然注意不要使用@SafeVarargs
注解标注一个方法。 那么须要作些什么来确保这一点呢? 回想一下,调用方法时会建立一个泛型数组,以容纳可变参数。 若是方法没有在数组中存储任何东西(它会覆盖参数)而且不容许对数组的引用进行转义(这会使不受信任的代码访问数组),那么它是安全的。 换句话说,若是可变参数数组仅用于从调用者向方法传递可变数量的参数——毕竟这是可变参数的目的——那么该方法是安全的。3d
值得注意的是,你能够违反类型安全性,即便不会在可变参数数组中存储任何内容。 考虑下面的泛型可变参数方法,它返回一个包含参数的数组。 乍一看,它可能看起来像一个方便的小工具:rest
// UNSAFE - Exposes a reference to its generic parameter array! static <T> T[] toArray(T... args) { return args; }
这个方法只是返回它的可变参数数组。 该方法可能看起来并不危险,但它是! 该数组的类型由传递给方法的参数的编译时类型决定,编译器可能没有足够的信息来作出正确的判断。 因为此方法返回其可变参数数组,它能够将堆污染传播到调用栈上。
为了具体说明,请考虑下面的泛型方法,它接受三个类型T
的参数,并返回一个包含两个参数的数组,随机选择:
static <T> T[] pickTwo(T a, T b, T c) { switch(ThreadLocalRandom.current().nextInt(3)) { case 0: return toArray(a, b); case 1: return toArray(a, c); case 2: return toArray(b, c); } throw new AssertionError(); // Can't get here }
这个方法自己不是危险的,除了调用具备泛型可变参数的toArray
方法以外,不会产生警告。
编译此方法时,编译器会生成代码以建立一个将两个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"); }
生成的代码是类型安全的,由于它只使用泛型,不是数组。
总而言之,可变参数和泛型不能很好地交互,由于可变参数机制是在数组上面构建的脆弱的抽象,而且数组具备与泛型不一样的类型规则。 虽然泛型可变参数不是类型安全的,但它们是合法的。 若是选择使用泛型(或参数化)可变参数编写方法,请首先确保该方法是类型安全的,而后使用@SafeVarargs
注解对其进行标注,以避免形成使用不愉快。