第31条:利用有限制通配符来提升API的灵活
在API中使用通配符类型(? extends 和 ? super)可以使参数类型更加灵活,同时保持类型安全。 这是实现"PECS"原则(Producer-Extends, Consumer-Super)的关键技术。
泛型不变性
如第 28 条所述,参数化类型是不变的(invariant)。换句话说,对于任何两个截然不同的类型 Type1和 Type2 而言,ist既不是 List的子类型,也不是它的超类型。虽然 List不是 List<0bject>的子类型,这与直觉相悖,但是实际上很有意义,你可以将任何对象放进一个List<0bject>中,却只能将字符串放进List中。由于 List不能像 List能做任何事情,它不是一个子类型(详见第 10 条)。
例如:
java
List<String> strings = new ArrayList<>();
List<Object> objects = strings; // 编译错误!泛型不变量
虽然String是Object的子类,但List不是List的子类,这是泛型的不变量性带来的限制。
两种通配符
上界通配符(Producer):? extends T
回顾第29条中我们写的自定义泛型stack代码(省略细节):
java
// 优先设计为泛型类
public class Stack<E>{
public Stack() {....}
public void push(E e) {....}
public E pop() {....}
}
在其中添加一个新方法pushAll
java
public void pushAll(Iterable<E> src) {
for (E e : src) {
push(e);
}
}
当这样使用时:
java
Stack<Number> stack = new Stack<>(10);
Iterable<Integer> src = Collections.singletonList(1);
stack.pushAll(src);

就会报错,正如开头所说,参数化类型不可变。
Java提供了一种特殊的参数化类型,称作有限制的通配符类型(bounded wildcard type),它可以处理类似的情况。pushAll的输人参数类型不应该为"E的 Iterable 接口",而应该为"E的某个子类型的Iterable接口"通配符类型Iterable<? extends E>正是这个意思。
java
public void pushAll(Iterable<? extends E> src) {
for (E e : src) {
push(e);
}
}
下界通配符(Consumer):? super T
编写popAll也是同样的道理。super和extends在泛型中正好反过来,这里表示E的某种超类的集合。
java
public void popAll(Collection<? super E> dst) {
while (size != 0) {
dst.add(pop());
}
}
结论
结论很明显:为了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型。如果某个输入参数既是生产者,又是消费者,那么通配符类型对你就没有什么好处了:因为你需要的是严格的类型匹配,这是不用任何通配符而得到的。
核心原则:PECS(Producer-Extends, Consumer-Super)
PECS含义:
- Producer(生产者):产生/提供数据 → 使用 ? extends T
- Consumer(消费者):消费/接受数据 → 使用 ? super T
要注意,这里的PECS中的"生产者"和"消费者"指的是参数本身的行为,而不是方法的功能,是向方法提供数据,还是从方法接收数据。如果使用得当,通配符类型对于用户应该是无感的。
- 如果类型参数在方法中既作为生产者又作为消费者,就不要使用通配符。通配符是为了扩大方法接受参数的范围,而不是限制。
- 所有的comparable和comparator都是消费者
看一个复杂的例子:
java
public static <T extends Comparable<? super T>> T max(List<? extends T> list)
- 返回类型T
- 方法参数:List<? extends T> list
- 类型参数约束:<T extends Comparable<? super T>>
-
- T extends Comparable<...> T必须实现Comparable接口,确保T可以被比较
-
- Comparable<? super T> T可以实现Comparable,也可以实现Comparable<T的父类>
优先使用通配符的情况
当类型参数在方法声明中只出现一次时,考虑使用通配符:
java
// 改进前
public static <E> void swap(List<E> list, int i, int j);
// 改进后(因为E只作为参数出现,没有其他依赖)
public static void swap(List<?> list, int i, int j);
java
// 直接实现会有问题
public static void swap(List<?> list, int i, int j) {
list.set(i, list.set(j, list.get(i))); // 编译错误!
}
// 通过私有辅助方法捕获类型
public static void swap(List<?> list, int i, int j) {
swapHelper(list, i, j);
}
// 辅助方法捕获通配符类型
private static <E> void swapHelper(List<E> list, int i, int j) {
list.set(i, list.set(j, list.get(i))); // √
}
泛型确实是一个很绕的东西。