第32项:谨慎地结合泛型和可变参数(Combine generics and varargs judiciously)

本文讨论了Java中泛型可变参数存在的类型安全问题,包括堆污染和编译器警告。解释了为什么使用泛型可变参数会引发警告,以及如何通过`@SafeVarargs`注解来消除警告。同时强调,只有当方法不会在可变参数数组中存储内容且不暴露数组时,使用`@SafeVarargs`才是安全的。最后,提出使用`List`参数替代可变参数来提高类型安全性。

  可变参数方法(第53项)和泛型都在Java 5时添加到了平台中,所以你可能会期望它们会优雅地相互作用;可悲的是,它们不能相互作用。可变的目的是允许客户端将数量可变的参数传递给方法,但它是一个漏洞抽象( leaky abstraction):当你调用可变参数方法时,会创建一个数组来保存可变参数;该数组应该是一个实现细节,是可见的。因此,当可变参数具有泛型或者参数化类型时,会出现令人困惑的编译器警告。

  回顾第28项,不可具体化类型(non-reifiable)是其运行时表示的信息少于其编译时表示的类型,并且几乎所有泛型和参数化类型都是不可恢复的。如果方法声明其可变参数为不可具体化类型,则编译器会在声明上生成警告。如果调用一个包含可变参数的方法时,推断其可变参数类型是不可具体化的,那么编译器也会对调用生成警告。警告如下所示:

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
}

  该方法没有可见(visible)的数据类型转换,但是在使用一个或多个参数调用时抛出ClassCastException。它的最后一行有一个由编译器生成的不可见转换。这种转换失败,说明类型安全性已经受到损害,并且在一般的可变参数数组中存储值是不安全的

  这个例子引出了一个有趣的问题:为什么使用泛型可变参数声明方法是合法的,而显示创建泛型数组是非法的?换句话说,为什么前面显示的方法只生成警告,而127页的代码片段生成错误?答案是,带有泛型或参数化类型的可变参数的方法在实践中非常有用,因此语言设计者选择了忍受这种不一致。事实上,Java库导出了好几个这样的方法,包括Arrays.asList(T... a), Collections.addAll(Collection<? super T> c, T... elements)EnumSet.of(E first, E... rest)。跟前面显示的危险方法不用,这些库方法都是类型安全的。

  在Java 7之前,使用泛型可变参数的方法的作者无法处理调用点(call sites)上的警告。这使得这些API使用起来不愉快。用户必须忍受警告,或者最好在没个调用点使用@SuppressWarnings("unchedked)注释消除警告(第27项)。这是乏味的,损害了可读性,并隐藏了标记真实问题的警告。

  在Java 7中,SafeVarargs注释已经添加到平台中,从而允许具有泛型可变参数的方法的作者可以自动压制客户端警告。本质上,SafeVarargs注释代表了该方法作者的一个承诺,它是类型安全的(the SafeVarargs annotation constitutes a promise by the author of a method that it is typesafe)。作为对此承诺的交换,编译器同意不会警告用户,调用该方法可能是不安全的。

  除非方法实际上是安全的,否则不要使用@SafeVarargs注释方法,这点至关重要。所以确保这一点【方法是安全的】需要什么呢?回想一下,在调用方法时会创建一个泛型数组,用来保存可变参数。如果方法没有将任何内容存储到数组中(这会覆盖参数)并且不允许对数组的引用进行转义(这会使不受信任的代码访问数组),那么它就是安全的。换句话说,如果可变参数数组仅用于从调用者向方法传递可变数量的参数——毕竟这是可变参数的目的——那么该方法就是安全的。

  值得注意的是,你可以违反类型安全,而无需在可变参数数组中存储任何内容。请考虑下面的泛型可变参数方法,该方法返回包含其参数的数组。乍一看,它似乎是一个方便的小实用程序:

// 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 []类型的数组。

  现在考虑下这个main方法,一个pickTwo方法的练习:

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

  这个方法没有任何问题,所以它编译时不会产生任何警告。但是当你运行它时,它会引发ClassCastException,尽管它不包含可见的强制转换。你没看到的是编译器在pickTwo返回的值上为String []生成了一个隐藏的强制转换,以便它可以存储在attributes中。转换失败,因为Object []不是String []的子类型。这种失败非常令人不安,因为它从实际导致堆污染的方法中删除了两个级别(toArray),并且实际参数存储在其中之后不会修改可变参数数组(This failure is quite disconcerting because it is two levels removed from the method that actually causes the heap pollution (toArray), and the varargs parameter array is not modified after the actual parameters are stored in it)。

  这个例子的目的是为了说明让另一个方法访问泛型可变参数数组是不安全的,这有两个例外:将数组传递给另一个正确使用@SafeVarargs注释的可变参数方法是安全的,并且它将数组传递给非可变参数方法是安全的,该非可变参数方法是指仅计算数组内容的某些函数。

  以下是安全使用泛型可变参数的典型示例。此方法将任意数量的list作为参数,并返回包含所有输入list元素的单个列表。由于该方法使用了@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,使用泛型或参数化类型的可变参数, 这样其用户就不用承担不必要和令人困惑的编译器警告的负担。这意味着你永远不应该编写像dangerous或toArray这样的不安全的可变参数方法。每次编译器在你控制的方法中警告你可能存在来自泛型可变参数的堆污染时,请检查该方法是否安全。提醒一下,如果符合以下条件,泛型可变参数方法是安全的:

  1、它不会在可变参数数组中存储任何内容。
  2、它不会使数组(或克隆出来的数组)对不受信任的代码可见。

  请注意,SafeVarargs注释仅对无法覆盖的方法是合法的,因为无法保证每个可能的重写方法都是安全的。在Java 8中,注释仅对静态方法和final的实例方法合法; 在Java 9中,它在private实例方法上也是合法的。

  使用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跟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对其进行注释,以免使用起来不开心。

第33项:优先考虑类型安全的异构容器

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值