可变参数方法( item53 )和泛型都是在Java5上新增的,所以你可能会期望他们能很好的互相互动;不幸的是,它们不是。可变参数的目的是允许客户端将可变数量的参数传递给方法,但是它是一个抽象泄露(leaky abstraction):当你调用一个可变参数方法,一个数组被创建来保留这些可变参数;实现细节的这个数组应该是可见的。因此,当可变参数具有泛型或参数化类型时,会收到令人困惑的编译器警告。
回想item28,不可还原类型是其运行时表示比其编译时表示少的类型,而且几乎所有泛型和参数化类型都是不可重用的。如果方法将其可变参数声明为不可还原类型,那么编译器将对声明生成警告。如果对可变参数调用该方法,而这些参数的推断类型是不可重用的,那么编译器也会在调用时生成一个警告。这些警告看起来是这样的:
image.png
当参数化类型的变量引用不属于该类型的对象时,就会发生堆污染 [JLS, 4.12.2]。它可能导致编译器自动生成的强制转换失败,违反了泛型类型系统的基本保证。
例如,考虑这个方法,它是页127代码片段的一个简单变体:
这个方法没有可见的强制转换,但是在使用一个或多个参数调用时会引发ClassCastException。它的最后一行有一个由编译器生成的不可见的强制转换。该转换失败,说明类型安全性已收到损害,在泛型多参数数组参数中存值是不安全的。
这个例子提出了一个有趣的问题:为什么在显式创建泛型数组是非法的情况下,使用泛型可变参数声明方法是合法的?换句话说,为什么前面显示的方法只生成了一个警告,而页127上的代码片段则生成了一个错误?答案是使用泛型或参数化类型的可变参数在实际中可能非常有用,所以语言设计者选择忍受这种不一致。事实上,Java库也导出了集中这样的方法,包括Arrays.asList(T... a), Collections.addAll(Collection<? super T> c,T... elements),和EnumSet.of(E first.E... rest)。与前面的dangerous方法不一样,这些库方法是类型安全的。
在Java7之前,使用泛型可变参数的方法的作者对调用站点上的警告无能为力。这使得这些api难以使用。用户不得不忍受这些警告,或者最好使用@SuppressWarnings("unchecked")注解来消除警告。这是乏味的,损害了可读性,并隐藏了标记真实问题的警告。
在Java7,SafeVarargs注解被加入了平台,来允许具有泛型可变参数的方法的作者自动抑制客户端的警告。本质上,SafeVarargs注解构成了一个方法的作者的承诺,即认为该方法是类型安全的。作为交换,编译器同意不警告用户调用可能不安全的方法。
重要的是你不能用@SafeVarargs对方法注释,除非它实际上是安全的。那么,如何才能确保这一点呢?回想一下,在调用方法时创建了一个泛型数组,以保存可变参数。如果方法没有将任何内容存储到数组中(这将覆盖数组),并且不允许对数组的引用转义(这将允许不受信任的代码访问数组),那么它是安全的。换句话说,如果可变参数数组仅用于将可变参数的参数从调用方传输到方法(毕竟,这是可变参数的目的),那么该方法是安全的。
值得注意的是,你不需要在可变参数数组中存储任何内容,就可以违反类型安全性。考虑以下泛型可变参数方法,它返回包含其参数的数组。乍一看,它看起来像是一个方便的小实用程序:
这个方法仅仅返回了它的可变参数数组。这种方法看起来并不危险,但是它是危险的!此数组的类型由传递给该方法的参数的编译时类型确定,编译器可能没有足够的信息来进行精确的确定。因为此方法返回其可变参数数组,所以它可以将堆污染传播到调用堆栈上。
使之具体化,考虑下列的泛型方法,该方法接受T类型的三个参数,并返回包含随机选择的两个参数的数组:
image.png
这个方法本身并不危险,也不会产生警告,除非它调用toArray方法,该方法有一个泛型多参数类型。
编译该方法时,编译器生成代码来创建多参数数组,将其中两个T实例传递给toArray。该代码分配一个类型为Object[]的数组,它是最能确保保存这些实例的类型,无论在调用方传递了什么类型的对象。toArray方法简单地将数组返回,并将它返回给它的调用方,所以pickTwo方法将总是返回一个类型为Object[]的数组。
现在,考虑下面这个方法,它实用了pickTwo方法:
image.png
这个方法没有任何问题,所以它编译的时候没有生成任何警告。但是当你运行的时候,它就会抛出ClassCastException,尽管它不包含可见的强制转换。你看不到的是编译器早已在背后让pickTwo返回的结果强制转换为String[]以便将其存储在属性中。强制转换失败了,因为Object[]不是String[]的子类型。这个失败非常令人不安,因为它是从实际造成堆污染的方法中移除的两个级别(toArray),并且可变参数数组在实际参数存储在其中之后不会被修改。
该示例的目的是让我们明白让另一个方法访问泛型可变参数数组是不安全的,但是有两个例外:将数组传递给另一个正确使用@SafeVarargs注解的方法是安全的,并且将数组传递给一个仅计算数组内容的某些函数的非多参数方法是安全的。
这是一个安全使用泛型可变参数的典型示例。这个方法以任意数量的列表作为参数,并返回一个按顺序排列的包含所有输入列表的元素的列表。由于该方法使用@SafeVarargs进行注解,因此它不会在声明或调用站点上生成任何警告:
决定如何使用SafeVarargs注解的规则很简单:在具有泛型或参数化类型的可变参数的每个方法上使用@SafeVarargs,因此,它的用户不会被不必要的和令人困惑的编译器警告所拖累。这意味着你应该永远不编写不安全的可变参数方法比如dangerous或toArray。每次编译器警告你控制的方法中可变参数可能造成堆污染时,请检查该方法是否安全。作为提醒,泛型可变参数方法在以下情况是安全的:
1.它不会在可变参数数组中存储任何内容,同时
2.它不会使数组(或克隆)对不受信任的代码可见。
如果这些禁令中的任何一项被违反,修复它。
请注意,SafeVarargs注解只有在对不能被覆写的方法是合法的,因为不可能保证每个覆写的方法都是安全的。在Java8中,该注解只在静态方法或final实例方法中合法;在Java9中,私有实例方法也是合法的。
使用SafeVarargs注解的另一种方法是接受item28的建议,将可变参数(它是一个伪装的数组)替换为一个list参数。下面是在flatten方法中的应用:
image.png
这个方法可以与静态工厂方法List.of结合使用,来允许参数的变量。注意到这个方法依赖于List.of声明使用@SafeVarargs的事实:
image.png
这个方法的好处是编译器可以证明这个方法是类型安全的。你不需要使用SafeVarargs注解来给这个方法作担保,你也不用担心你在认定它是安全的时候犯错误。但这方法的主要缺点是客户端代码更冗长,而且速度可能更慢一点。
这个技巧也可以在不可以编写安全的可变参数方法的情况下使用,就像第147页的toArray方法一样的情况。它的List模拟了List.of的方法,所以我们甚至不用编写它。Java库的作者已经为我们完成了这项工作。pickTwo方法变成了这样:
image.png
主方法变成了这样:
image.png
结果代码是类型安全的因为它只使用了泛型,没有使用数组。
总之,可变参数和泛型不能很好的相互作用,因为可变参数工具是构件在数组上的一个漏泄的抽象,而且数组与泛型有不同的类型规则。虽然泛型可变参数不是类型安全的,但他们是合法的。如果你选择使用泛型(或参数化)可变参数编写方法,首先确保这个方法是类型安全的,加上@SafeVarargs注解,因此使用它并不令人不快。
本文写于2019.7.3,历时2天
网友评论