Effective Java 第三版——31.使用限定通配符来增加API的灵活性

时间:2021-11-21 14:41:13

Tips

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

在这里第一时间翻译成中文版。供大家学习分享之用。

Effective Java 第三版——31.使用限定通配符来增加API的灵活性

31. 使用限定通配符来增加API的灵活性

如条目 28所述,参数化类型是不变的。换句话说,对于任何两个不同类型的Type1TypeList <Type1>既不是List <Type2>子类型也不是其父类型。尽管List <String>不是List <Object>的子类型是违反直觉的,但它确实是有道理的。 可以将任何对象放入List <Object>中,但是只能将字符串放入List <String>中。 由于List <String>不能做List <Object>所能做的所有事情,所以它不是一个子类型(条目 10 中的里氏替代原则)。

相对于提供的不可变的类型,有时你需要比此更多的灵活性。 考虑条目 29中的Stack类。下面是它的公共API:

public class Stack<E> {

    public Stack();

    public void push(E e);

    public E pop();

    public boolean isEmpty();

}

假设我们想要添加一个方法来获取一系列元素,并将它们全部推送到栈上。 以下是第一种尝试:

// pushAll method without wildcard type - deficient!

public void pushAll(Iterable<E> src) {
for (E e : src)
push(e);
}

这种方法可以干净地编译,但不完全令人满意。 如果可遍历的src元素类型与栈的元素类型完全匹配,那么它工作正常。 但是,假设有一个Stack <Number>,并调用push(intVal),其中intVal的类型是Integer。 这是因为IntegerNumber的子类型。 从逻辑上看,这似乎也应该起作用:

Stack<Number> numberStack = new Stack<>();

Iterable<Integer> integers = ... ;

numberStack.pushAll(integers);

但是,如果你尝试了,会得到这个错误消息,因为参数化类型是不变的:

StackTest.java:7: error: incompatible types: Iterable<Integer>

cannot be converted to Iterable<Number>

        numberStack.pushAll(integers);

                            ^

幸运的是,有对应的解决方法。 该语言提供了一种特殊的参数化类型来调用一个限定通配符类型来处理这种情况。 pushAll的输入参数的类型不应该是“E的Iterable接口”,而应该是“E的某个子类型的Iterable接口”,并且有一个通配符类型,这意味着:Iterable <? extends E>。 (关键字extends的使用有点误导:回忆条目 29中,子类型被定义为每个类型都是它自己的子类型,即使它本身没有继承。)让我们修改pushAll来使用这个类型:

// Wildcard type for a parameter that serves as an E producer

public void pushAll(Iterable<? extends E> src) {
for (E e : src)
push(e);
}

有了这个改变,Stack类不仅可以干净地编译,而且客户端代码也不会用原始的pushAll声明编译。 因为Stack和它的客户端干净地编译,你知道一切都是类型安全的。

现在假设你想写一个popAll方法,与pushAll方法相对应。 popAll方法从栈中弹出每个元素并将元素添加到给定的集合中。 以下是第一次尝试编写popAll方法的过程:

// popAll method without wildcard type - deficient!

public void popAll(Collection<E> dst) {

    while (!isEmpty())

        dst.add(pop());

}

同样,如果目标集合的元素类型与栈的元素类型完全匹配,则干净编译并且工作正常。 但是,这又不完全令人满意。 假设你有一个Stack <Number>Object类型的变量。 如果从栈中弹出一个元素并将其存储在该变量中,它将编译并运行而不会出错。 所以你也不能这样做吗?

Stack<Number> numberStack = new Stack<Number>();

Collection<Object> objects = ... ;

numberStack.popAll(objects);

如果尝试将此客户端代码与之前显示的popAll版本进行编译,则会得到与我们的第一版pushAll非常类似的错误:Collection <Object>不是Collection <Number>的子类型。 通配符类型再一次提供了一条出路。 popAll的输入参数的类型不应该是“E的集合”,而应该是“E的某个父类型的集合”(其中父类型被定义为E是它自己的父类型[JLS,4.10])。 再次,有一个通配符类型,正是这个意思:Collection <? super E>。 让我们修改popAll来使用它:

// Wildcard type for parameter that serves as an E consumer

public void popAll(Collection<? super E> dst) {

    while (!isEmpty())

        dst.add(pop());

}

通过这个改动,Stack类和客户端代码都可以干净地编译。

这个结论很清楚。 为了获得最大的灵活性,对代表生产者或消费者的输入参数使用通配符类型。 如果一个输入参数既是一个生产者又是一个消费者,那么通配符类型对你没有好处:你需要一个精确的类型匹配,这就是没有任何通配符的情况。

这里有一个助记符来帮助你记住使用哪种通配符类型:

PECS代表: producer-extends,consumer-super。

换句话说,如果一个参数化类型代表一个T生产者,使用 <? extends T>;如果它代表T消费者,则使用<? super T>。 在我们的Stack示例中,pushAll方法的src参数生成栈使用的E实例,因此src的合适类型为Iterable<? extends E>popAll方法的dst参数消费Stack中的E实例,因此dst的合适类型是Collection <? super E>。 PECS助记符抓住了使用通配符类型的基本原则。 Naftalin和Wadler称之为获取和放置原则( Get and Put Principle )[Naftalin07,2.4]。

记住这个助记符之后,让我们来看看本章中以前项目的一些方法和构造方法声明。 条目 28中的Chooser类构造方法有这样的声明:

public Chooser(Collection<T> choices)

这个构造方法只使用集合选择来生产类型T的值(并将它们存储起来以备后用),所以它的声明应该使用一个 extends T的通配符类型。下面是得到的构造方法声明:

// Wildcard type for parameter that serves as an T producer

public Chooser(Collection<? extends T> choices)

这种改变在实践中会有什么不同吗? 是的,会有不同。 假你有一个List <Integer>,并且想把它传递给Chooser<Number>的构造方法。 这不会与原始声明一起编译,但是它只会将限定通配符类型添加到声明中。

现在看看条目 30中的union方法。下是声明:

public static <E> Set<E> union(Set<E> s1, Set<E> s2)

两个参数s1s2都是E的生产者,所以PECS助记符告诉我们该声明应该如下:

public static <E> Set<E> union(Set<? extends E> s1,  Set<? extends E> s2)

请注意,返回类型仍然是Set <E>。 不要使用限定通配符类型作为返回类型。除了会为用户提供额外的灵活性,还强制他们在客户端代码中使用通配符类型。 通过修改后的声明,此代码将清晰地编译:

Set<Integer>  integers =  Set.of(1, 3, 5);

Set<Double>   doubles  =  Set.of(2.0, 4.0, 6.0);

Set<Number>   numbers  =  union(integers, doubles);

如果使用得当,类的用户几乎不会看到通配符类型。 他们使方法接受他们应该接受的参数,拒绝他们应该拒绝的参数。 如果一个类的用户必须考虑通配符类型,那么它的API可能有问题。

在Java 8之前,类型推断规则不够聪明,无法处理先前的代码片段,这要求编译器使用上下文指定的返回类型(或目标类型)来推断E的类型。union方法调用的目标类型如前所示是Set <Number>。 如果尝试在早期版本的Java中编译片段(以及适合的Set.of工厂替代版本),将会看到如此长的错综复杂的错误消息:

Union.java:14: error: incompatible types

        Set<Number> numbers = union(integers, doubles);

                                   ^

  required: Set<Number>

  found:    Set<INT#1>

  where INT#1,INT#2 are intersection types:

    INT#1 extends Number,Comparable<? extends INT#2>

    INT#2 extends Number,Comparable<?>

幸运的是有办法来处理这种错误。 如果编译器不能推断出正确的类型,你可以随时告诉它使用什么类型的显式类型参数[JLS,15.12]。 甚至在Java 8中引入目标类型之前,这不是你必须经常做的事情,这很好,因为显式类型参数不是很漂亮。 通过添加显式类型参数,如下所示,代码片段在Java 8之前的版本中进行了干净编译:

// Explicit type parameter - required prior to Java 8

Set<Number> numbers = Union.<Number>union(integers, doubles);

接下来让我们把注意力转向条目 30中的max方法。这里是原始声明:

public static <T extends Comparable<T>> T max(List<T> list)

以下是使用通配符类型的修改后的声明:

public static <T extends Comparable<? super T>> T max(List<? extends T> list)

为了从原来到修改后的声明,我们两次应用了PECS。首先直接的应用是参数列表。 它生成T实例,所以将类型从List <T>更改为List<? extends T>。 棘手的应用是类型参数T。这是我们第一次看到通配符应用于类型参数。 最初,T被指定为继承Comparable <T>,但ComparableT消费T实例(并生成指示顺序关系的整数)。 因此,参数化类型Comparable <T>被替换为限定通配符类型 Comparable<? super T>Comparable实例总是消费者,所以通常应该使用Comparable<? super T>优于Comparable <T>Comparator也是如此。因此,通常应该使用Comparator<? super T> 优于Comparator<T>

修改后的max声明可能是本书中最复杂的方法声明。 增加的复杂性是否真的起作用了吗? 同样,它的确如此。 这是一个列表的简单例子,它被原始声明排除,但在被修改后的版本里是允许的:

List<ScheduledFuture<?>> scheduledFutures = ... ;

无法将原始方法声明应用于此列表的原因是ScheduledFuture不实现Comparable <ScheduledFuture>。 相反,它是Delayed的子接口,它继承了Comparable <Delayed>。 换句话说,一个ScheduledFuture实例不仅仅和其他的ScheduledFuture实例相比较: 它可以与任何Delayed实例比较,并且足以导致原始的声明拒绝它。 更普遍地说,通配符要求来支持没有直接实现Comparable(或Comparator)的类型,但继承了一个类型。

还有一个关于通配符相关的话题。 类型参数和通配符之间具有双重性,许多方法可以用一个或另一个声明。 例如,下面是两个可能的声明,用于交换列表中两个索引项目的静态方法。 第一个使用无限制类型参数(条目 30),第二个使用无限制通配符:

// Two possible declarations for the swap method

public static <E> void swap(List<E> list, int i, int j);

public static void swap(List<?> list, int i, int j);

这两个声明中的哪一个更可取,为什么? 在公共API中,第二个更好,因为它更简单。 你传入一个列表(任何列表),该方法交换索引的元素。 没有类型参数需要担心。 通常,如果类型参数在方法声明中只出现一次,请将其替换为通配符。 如果它是一个无限制的类型参数,请将其替换为无限制的通配符; 如果它是一个限定类型参数,则用限定通配符替换它。

第二个swap方法声明有一个问题。 这个简单的实现不会编译:

public static void swap(List<?> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}

试图编译它会产生这个不太有用的错误信息:

Swap.java:5: error: incompatible types: Object cannot be

converted to CAP#1

        list.set(i, list.set(j, list.get(i)));

                                        ^

  where CAP#1 is a fresh type-variable:

    CAP#1 extends Object from capture of ?

看起来我们不能把一个元素放回到我们刚刚拿出来的列表中。 问题是列表的类型是List <?>,并且不能将除null外的任何值放入List <?>中。 幸运的是,有一种方法可以在不使用不安全的转换或原始类型的情况下实现此方法。 这个想法是写一个私有辅助方法来捕捉通配符类型。 辅助方法必须是泛型方法才能捕获类型。 以下是它的定义:

public static void swap(List<?> list, int i, int j) {

    swapHelper(list, i, j);

}

// Private helper method for wildcard capture

private static <E> void swapHelper(List<E> list, int i, int j) {

    list.set(i, list.set(j, list.get(i)));

}

swapHelper方法知道该列表是一个List <E>。 因此,它知道从这个列表中获得的任何值都是E类型,并且可以安全地将任何类型的E值放入列表中。 这个稍微复杂的swap的实现可以干净地编译。 它允许我们导出基于通配符的漂亮声明,同时利用内部更复杂的泛型方法。 swap方法的客户端不需要面对更复杂的swapHelper声明,但他们从中受益。 辅助方法具有我们认为对公共方法来说过于复杂的签名。

总之,在你的API中使用通配符类型,虽然棘手,但使得API更加灵活。 如果编写一个将被广泛使用的类库,正确使用通配符类型应该被认为是强制性的。 记住基本规则: producer-extends, consumer-super(PECS)。 还要记住,所有ComparableComparator都是消费者。

Effective Java 第三版——31.使用限定通配符来增加API的灵活性的更多相关文章

  1. Effective Java 第三版——56&period; 为所有已公开的API元素编写文档注释

    Tips 书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code 注意,书中的有些代码里方法是基于Java 9 API中的,所 ...

  2. effective-java学习笔记---使用限定通配符来增加 API 的灵活性31

    在你的 API 中使用通配符类型,虽然棘手,但使得 API 更加灵活. 如果编写一个将被广泛使用的类库,正确使用通配符类型应该被认为是强制性的. 记住基本规则: producer-extends, c ...

  3. 《Effective Java 第三版》目录汇总

    经过反复不断的拖延和坚持,所有条目已经翻译完成,供大家分享学习.时间有限,个别地方翻译得比较仓促,希望有疑虑的地方指出批评改正. 第一章简介 忽略 第二章 创建和销毁对象 1. 考虑使用静态工厂方法替 ...

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

    版权声明:本文为博主原创文章,可以随意转载,不过请加上原文链接. https://blog.csdn.net/u014717036/article/details/80588806前言 从去年的3月份 ...

  5. Effective Java 第三版——30&period; 优先使用泛型方法

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  6. Effective Java 第三版——11&period; 重写equals方法时同时也要重写hashcode方法

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  7. Effective Java 第三版——22&period; 接口仅用来定义类型

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  8. Effective Java 第三版——24&period; 优先考虑静态成员类

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  9. Effective Java 第三版——33&period; 优先考虑类型安全的异构容器

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

随机推荐

  1. ContentType ,charset和pageEncoding的区别&lpar;转&rpar;

    ========================说法一=========================== ContentType 属性指定响应的 HTTP 内容类型.如果未指定 ContentTy ...

  2. 【原创】驱动枚举之EnumServicesStatusEx

    BOOL WINAPI EnumServicesStatusEx( _In_ SC_HANDLE hSCManager, _In_ SC_ENUM_TYPE InfoLevel, _In_ DWORD ...

  3. hibernate04--三种状态之间的转换

    public class StudentTest { Session session=null; Transaction transaction=null; //在执行测试方法之前 先执行before ...

  4. Luogu 1083 借教室(二分,差分)

    Luogu 1083 借教室(二分,差分) Description 在大学期间,经常需要租借教室.大到院系举办活动,小到学习小组自习讨论,都需要 向学校申请借教室.教室的大小功能不同,借教室人的身份不 ...

  5. Ubuntu系统添加搜狗输入法

    前端开发时有时候要接触到Ubuntu系统,但由于本身没有拼音输入,故需要自己安装搜狗,记录方法如下: 1.安装前先升级资源库并安装输入法依赖包: $sudo apt-get update $sudo ...

  6. react将表格动态生成视频列表【代码】【案例】

    只需要创建一个表格,id为videos,react就能将这个表格转换成视频列表,并点击自动播放 index.html <!DOCTYPE html> <html> <he ...

  7. ansible 的user模块

    user模块与group模块 user模块是请求的是useradd, userdel, usermod三个指令,goup模块请求的是groupadd, groupdel, groupmod 三个指令. ...

  8. 雷林鹏分享:Ruby Dir 类和方法

    Ruby Dir 类和方法 Dir 是一个表示用于给出操作系统中目录中的文件名的目录流.Dir 类也拥有与目录相关的操作,比如通配符文件名匹配.改变工作目录等. 类方法 序号方法 & 描述 1 ...

  9. Spring Cloud对于中小型互联网公司来说是一种福音

    Spring Cloud对于中小型互联网公司来说是一种福音,因为这类公司往往没有实力或者没有足够的资金投入去开发自己的分布式系统基础设施,使用Spring Cloud一站式解决方案能在从容应对业务发展 ...

  10. 将封装了envi功能的IDL类导出成java类,方便java调用

    目的:     用IDL将ENVI的功能封装成为IDL的类,并使用IDL的对象导出功能把这些功能类导出为java类,方便java调用.(本来想直接通过GP工具调用的,但是没有授权文件)   操作步骤: ...