为了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型。如果某个输入参数既是生产者,又是消费者,那么通配符类型对你就没有什么好处了:因为你需要的是严格的类型匹配,这是不用任何通配符而得到的。下面的助记符便于让你记住要使用哪种通配符类型:PECS 表示 producer-extends,consumer-super。
换句话说,如果参数化类型表示一个生产者 T,就使用<?extends T>;如果它表示一个消费者 T,就使用<?super T>。在我们的 Stack 示例中,pushAll的 src 参数产生日实例供 Stack 使用,因此 src 相应的类型为Iterable<? extends E>;popAl1
的 dst 参数通过 Stack 消费日实例,因此 dst 相应的类型为Collection<?super E>。PECS 这个助记符突出了使用通配符类型的基本原则。Naftalin 和 Wadler 称之为 Get and Put Principle [Naftalin07, 2.4]。
记住这个助记符,下面我们来看一些之前的条目中提到过的方法声明。第 28 条中的reduce 方法就有这条声明:
public Chooser(Collection<T> choices)
这个构造器只用 choices 集合来生成类型 T 的值(并把它们保存起来供后续使用),因此它的声明应该使用一个 extends T 的通配符类型。得到的构造器声明如下:
// Wildcard type for parameter that serves as an T producer
public Chooser(Collection<? extends T> choices)
这一变化实际上有什么区别吗?事实上,的确有区别。假设你有一个List<Integer>,
想通过Function<Number>把它简化。它不能通过初始声明进行编译,但是一旦添加了有限制的通配符类型,就可以进行编译了。现在让我们看看第 30 条中的 union 方法。声明如下:
public static <E> Set<E> union(Set<E> s1, Set<E> s2)
s1 和 s2 这两个参数都是生产者 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> doubies = Set.of(2.0, 4.0, 6.0);
Set<Number> numbers = union(integers, doubles);
如果使用得当,通配符类型对于类的用户来说几乎是无形的。它们使方法能够接受它们应该接受的参数,并拒绝那些应该拒绝的参数。如果类的用户必须考虑通配符类型,类的API 或许就会出错。