合理地结合泛型和可变参数
在 Java 5 中,可变参数方法和泛型都被添加到平台中,所以你可能希望它们能够正常交互; 可悲的是,他们并没有。
可变参数的目的是允许客户端将一个可变数量的参数传递给一个方法,但这是一个脆弱的抽象(leaky abstraction):当你调用一个可变参数方法时,会创建一个数组来保存可变参数;那个应该是实现细节的数组是可见的。 因此,当可变参数具有泛型或参数化类型时,会导致编译器警告混淆。
非具体化(non-reifiable)的类型是其运行时表示比其编译时表示具有更少信息的类型,并且几乎所有泛型和参数化类型都是不可具体化的。 如果某个方法声明其可变参数为非具体化的类型,则编译器将在该声明上生成警告。 如果在推断类型不可确定的可变参数参数上调用该方法,那么编译器也会在调用中生成警告。
当参数化类型的变量引用不属于该类型的对象时会发生堆污染(Heap pollution)。它会导致编译器的自动生成的强制转换失败,违反了泛型类型系统的基本保证。
示例代码:Item32Example01.java:此方法没有可见的强制转换,但在调用一个或多个参数时抛出 ClassCastException
异常。 它的最后一行有一个由编译器生成的隐形转换。 这种转换失败,表明类型安全性已经被破坏,并且将值保存在泛型可变参数数组参数中是不安全的。
在 Java 7 中,@SafeVarargs
注解已添加到平台,以允许具有泛型可变参数的方法的作者自动禁止客户端警告。 实质上,@SafeVarargs
注解构成了作者对类型安全的方法的承诺。 为了交换这个承诺,编译器同意不要警告用户调用可能不安全的方法。
除非它实际上是安全的,否则注意不要使用 @SafeVarargs
注解标注一个方法。 那么需要做些什么来确保这一点呢? 回想一下,调用方法时会创建一个泛型数组,以容纳可变参数。 如果方法没有在数组中存储任何东西(它会覆盖参数)并且不允许对数组的引用进行转义(这会使不受信任的代码访问数组),那么它是安全的。 换句话说,如果可变参数数组仅用于从调用者向方法传递可变数量的参数——毕竟这是可变参数的目的——那么该方法是安全的。
- Item32Example02.java:这种方法没有任何问题,因此它编译时不会产生任何警告。 但是当运行它时,抛出一个
ClassCastException
异常,尽管不包含可见的转换。 你没有看到的是,编译器已经生成了一个隐藏的强制转换为由pickTwo
返回的值的String[]
类型,以便它可以存储在属性中。 转换失败,因为Object[]
不是String[]
的子类型。 这种故障相当令人不安,因为它从实际导致堆污染(toArray
)的方法中移除了两个级别,并且在实际参数存储在其中之后,可变参数数组未被修改。 - Item32Example03.java:安全使用泛型可变参数的典型示例。 此方法将任意数量的列表作为参数,并按顺序返回包含所有输入列表元素的单个列表。 由于该方法使用 @SafeVarargs 进行标注,因此在声明或其调用站位置上不会生成任何警告
- Item32Example04.java:生成的代码是类型安全的,因为它只使用泛型,不是数组。
决定何时使用 @SafeVarargs
注解的规则很简单:在每种方法上使用 @SafeVarargs
,并使用泛型或参数化类型的可变参数,这样用户就不会因不必要的和令人困惑的编译器警告而担忧。 这意味着你不应该写危险或者 toArray
等不安全的可变参数方法。 每次编译器警告你可能会受到来自你控制的方法中泛型可变参数的堆污染时,请检查该方法是否安全。 提醒一下,在下列情况下,泛型可变参数方法是安全的:
- 它不会在可变参数数组中存储任何东西
- 它不会使数组(或克隆)对不可信代码可见。 如果违反这些禁令中的任何一项,请修复。
总而言之,可变参数和泛型不能很好地交互,因为可变参数机制是在数组上面构建的脆弱的抽象,并且数组具有与泛型不同的类型规则。 虽然泛型可变参数不是类型安全的,但它们是合法的。 如果选择使用泛型(或参数化)可变参数编写方法,请首先确保该方法是类型安全的,然后使用 @SafeVarargs
注解对其进行标注,以免造成使用不愉快。