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


参数化类型是不变的。换句话说,对于任何两个不同类型的 Type1
Type2 List 既不是 List 的子类型也不是其父类型。尽管 List
不是 List 的子类型是违反直觉的,但它确实是有道理的。 可以将任何对象放入
List 中,但是只能将字符串放入 List 中。 由于 List 不能做
List 所能做的所有事情,所以它不是一个子类型

public class Stack {
public Stack();
public void push(E e);
public E pop();
public boolean isEmpty();
}

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

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

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

Stack numberStack = new Stack<>();
Iterable 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 来使用这个类型

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

  有了这个改变, Stack 类不仅可以干净地编译,而且客户端代码也不会用原始的 pushAll 声明编
译。 因为 Stack 和它的客户端干净地编译,你知道一切都是类型安全的。
这个结论很清楚。 为了获得最大的灵活性,对代表生产者或消费者的输入参数使用通配符类型。
如果一个输入参数既是一个生产者又是一个消费者,那么通配符类型对你没有好处:你需要一个精确的
类型匹配,这就是没有任何通配符的情况

这里有一个助记符来帮助你记住使用哪种通配符类型: PECS 代表: producer-extendsconsumersuper
换句话说,如果一个参数化类型代表一个 T 生产者,使用 <? extends T> ;如果它代表 T
消费者,则使用 <? super T>