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

Tips
《Effective Java, Third Edition》一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深刻的变化。
在这里第一时间翻译成中文版。供大家学习分享之用。

32. 合理地结合泛型和可变参数

在Java 5中,可变参数方法(条目 53)和泛型都被添加到平台中,所以你可能希望它们能够正常交互; 可悲的是,他们并没有。 可变参数的目的是允许客户端将一个可变数量的参数传递给一个方法,但这是一个脆弱的抽象( 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
}

此方法没有可见的强制转换,但在调用一个或多个参数时抛出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注解标注一个方法。 那么需要做些什么来确保这一点呢? 回想一下,调用方法时会创建一个泛型数组,以容纳可变参数。 如果方法没有在数组中存储任何东西(它会覆盖参数)并且不允许对数组的引用进行转义(这会使不受信任的代码访问数组),那么它是安全的。 换句话说,如果可变参数数组仅用于从调用者向方法传递可变数量的参数——毕竟这是可变参数的目的——那么该方法是安全的。

值得注意的是,你可以违反类型安全性,即使不会在可变参数数组中存储任何内容。 考虑下面的泛型可变参数方法,它返回一个包含参数的数组。 乍一看,它可能看起来像一个方便的小工具:

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

现在考虑这个测试pickTwmain方法:

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注解对其进行标注,以免造成使用不愉快。

原文地址:https://www.cnblogs.com/IcanFixIt/p/8444200.html

时间: 2024-10-16 07:15:35

Effective Java 第三版——32.合理地结合泛型和可变参数的相关文章

《Effective Java 第三版》新条目介绍

前言 从去年的3月份起我就在开始读<Effective Java 第二版>,当然,我读的是中文版的:可能是我理解能力还不行,对于书中的内容总是感觉理解困难:就拿第一章的内容「创建和销毁对象」来说吧,这是我读的次数最多的一章,想必原因大家也是明白的,每次我读不下去的时候,我就从头开始读,所以,现在我对这本书的第一章是最为熟悉的了.后来,有一次我上网看到有网友说这本书确实和绝大部分的翻译书籍一样,对于有些原文中的内容翻译的不是很流畅,所以会导致阅读的人感觉难以理解:于是,我就斗胆下了本英文的原版来

Effective Java 第三版——3. 使用私有构造方法或枚类实现Singleton属性

Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深刻的变化. 在这里第一时间翻译成中文版.供大家学习分享之用. 3. 使用私有构造方法或枚类实现Singleton属性 单例是一个仅实例化一次的类[Gamma95].单例对象通常表示无状态对象,如函数(条目 24)或一个本质上唯一的系统

Effective Java 第三版——10. 重写equals方法时遵守通用约定

Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深刻的变化. 在这里第一时间翻译成中文版.供大家学习分享之用. 10. 重写equals方法时遵守通用约定 虽然Object是一个具体的类,但它主要是为继承而设计的.它的所有非 final方法(equals.hashCode.toStr

Effective Java 第三版——12. 始终重写 toString 方法

Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深刻的变化. 在这里第一时间翻译成中文版.供大家学习分享之用. 12. 始终重写 toString 方法 虽然Object类提供了toString方法的实现,但它返回的字符串通常不是你的类的用户想要看到的. 它由类名后跟一个"

Effective Java 第三版——14.考虑实现Comparable接口

Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深刻的变化. 在这里第一时间翻译成中文版.供大家学习分享之用. ?14.考虑实现Comparable接口 与本章讨论的其他方法不同,compareTo方法并没有在Object类中声明. 相反,它是Comparable接口中的唯一方法.

Effective Java 第三版——18. 组合优于继承

Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深刻的变化. 在这里第一时间翻译成中文版.供大家学习分享之用. 18. 组合优于继承 继承是实现代码重用的有效方式,但并不总是最好的工具.使用不当,会导致脆弱的软件. 在包中使用继承是安全的,其中子类和父类的实现都在同一个程序员的控制之

Effective Java 第三版——19. 如果使用继承则设计,并文档说明,否则不该使用

Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深刻的变化. 在这里第一时间翻译成中文版.供大家学习分享之用. 19. 如果使用继承则设计,并文档说明,否则不该使用 条目 18中提醒你注意继承没有设计和文档说明的"外来"类的子类化的危险. 那么为了继承而设计和文档

Effective Java 第三版——20. 接口优于抽象类

Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深刻的变化. 在这里第一时间翻译成中文版.供大家学习分享之用. 20. 接口优于抽象类 Java有两种机制来定义允许多个实现的类型:接口和抽象类. 由于在Java 8 [JLS 9.4.3]中引入了接口的默认方法(default met

Effective Java 第三版——21. 为后代设计接口

Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深刻的变化. 在这里第一时间翻译成中文版.供大家学习分享之用. 21. 为后代设计接口 在Java 8之前,不可能在不破坏现有实现的情况下为接口添加方法. 如果向接口添加了一个新方法,现有的实现通常会缺少该方法,从而导致编译时错误. 在