正如item28所指出得,参数化类型是不变的。还击话说,对于type1和type2这两种不同的类型,List
假设我们要添加一个方法,该方法接受一系列元素并将他们全部推到Stack上。如下是第一次尝试:
这种方法编译清晰,但并不完全令人满意。如果迭代器src的元素类型与Stack的类型匹配,它能正常工作。但是假设你有Stack
如果你这么尝试了,你会得到如下错误信息,因为参数化类型是不可变的:
幸运的是,有一条出路。语言提供了一种特殊的参数化类型调用-有界通配符类型,以处理这种情况。输入参数的类型应该不是“Iterable of E”而是“Iterable of some subtype of E”.有一种通配符类型,它的确切意思是:Iterable extends E>.(extends关键字的使用是略微误导了:回想一下item29,即定义了子类型,以便每个类型都是自身的一个子类型,即使它不扩展自己),让我们一起修改pushAll来使用这个类型:
随着此更改,Stack不仅可以干净地编译,而且不会使用原来的pushAll声明编译的客户端代码也一样。因为Stack及其客户端编译干净,所以你知道所有的东西都是类型安全的。
现在假设你想要编写一个popAll方法与pushAll类似。popAll方法弹出Stack的每个元素,并将元素添加到给定的集合中。这是如何编写popAll方法的初次尝试的样子:
同样,编译干净切工作良好,如果目标集合的元素类型与Stack的元素类型完全匹配。但是同样,这并不完全令人满意。假设你有Stack
如果你尝试根据前面显示的popAll版本编译该客户端代码,你将得到一个非常类似我们使用第一个版本的pushAll:Collection
随着这个改变,Stack和客户端代码编译都很清晰。
这个教训很清晰。为了最大的灵活性,在输入参数上使用通配符类型代表了生产者或消费者。如果一个输入参数都是生产者和一个消费者,那么通配符类型对你没有好处:你需要一个精确的类型匹配,这就是没有任何通配符的情况。
下面是帮助你记住使用哪种通配符类型的助记符:
PECS stands for producer-extends, consumer-super
换句话说,如果一个参数化类型代表了T生产者,使用 extends T>;如果代表了一个T 消费者,使用 super T>.在我们的Stack 例子中,通过Stack的pushAll的src参数生产者E实例的使用,所以src的合适的类型是Iterable extends E>;popAll的dst参数从stack消费E实例,所以dst的合适的类型是Collection super E>.PESC助记符捕获了指导通配符类型使用的基本原则。Naftalin和Wadler称其为GET和PUT原则 [Naftalin07, 2.4]。
随着内心有着这个助记符,让我们看看在本章前几项中的一些方法和构造方法声明。在item28中的Chooser构造方法有如下声明:
这个构造方法只使用集合选项来生成T类型的值(并存储它们供以后使用),因此它的声明应该使用扩展T的通配符类型。下面是生成的构造方法声明:
这种变化在实际上有改变吗?是的,它有。假设你有一个List
现在,让我们一起从Item30中看union方法。这是声明:
s1和s2和生产者E,根据PECS助记告诉我们声明应该是这样的:
注意到返回类型仍然是Set
如果使用得当,类的用户几乎看不到通配符类型。它们导致方法接受它们应该接受的参数,并拒绝它们应该拒绝的参数。如果类的用户不得不考虑通配符类型,可能是API出错了。
在Java8之前,类型推断规则并不足够智能,无法处理前面的代码片段,这要求编译器使用上限为指定的返回类型(或目标类型)来推断E的类型。前面显示的联合调用的目标类型是Set
幸运的是,有一种方法可以处理这类错误。如果编译器不能推断正确的类型,你可以总是告诉它在显示类型中使用哪种类型 [JLS, 15.12]。即使在Java8中引入目标类型之前,这也不是你必须经常做的事情,这很好,因为显式类型参数不是很漂亮。通过添加显式类型参数(如此处所示),代码片段在Java8之前的版本中可以清晰地编译:
接下来,让我们关注item30的max方法,这是最初的声明:
这是使用通配符类型修改的声明:
为了得到修改后的声明,我们使用了两次PECS的启发。简单的应用程序是参数列表。它生成T实例,所以我们将类型从List
修订后的max声明可能是本书中最复杂的方法声明。增加的复杂性真的给你带来了什么吗?再说一次,的确如此。下面是一个清单的简单例子,它将被原始声明排除在外,但经修订的声明却允许这样做:
不能将原始方法声明应用于此列表的原因是ScheduledFuture并没有实现Comparable
还有一个与通配符相关的话题指的讨论。类型参数与通配符之间存在二元性,可以使用其中一种或另一种来声明许多方法。例如,下面是用于交换列表中两个索引项的静态方法的两个可能的声明。第一个参数使用无界类型参数( item30),第二个参数使用无界通配符。
这两种声明哪个更可取,为什么?在公有API,第二个更好,因为它更简单。你传递了一个列表-任何列表-方法叫喊了元素的索引。没有类型参数来担心。作为一个规则,如果类型参数在方法声明中只出现一次,则用通配符替换它。如果它是一个无界类型参数,用一个无界通配符替换它;如果它是有界类型参数,则用有界通配符替换它。
swap的第二个声明有一个问题。简单的实现不会编译。
试图编译它会产生以下不太有用的错误消息:
似乎我们不能把一个元素放回我们刚刚从列表中删除的列表中,这似乎是不对的。问题在于,list的类型是List>,不能将除了null以外的任何值放入List>.幸运的是,有一种方法可以实现此方法,而不必求助于不安全的类型或原始类型。这个想法是编写一个私有辅助方法来捕获通配符类型。这个辅助方法必须是为了捕获类型的泛型方法。这是它的样子:
swapHelper方法知道list的类型是List
总之,在你的APIs中使用通配符类型,虽然有点棘手但是使得API更加灵活。如果编写了一个将被广泛使用的库,则应该认为正确使用通配符类型是强制性的。记住基本规则:producer-extends, consumer-super (PECS)。也记住所有可比较的和比较器都是消费者。
本文写于2019.6.27,历时1天