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

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

相对于提供的不可变的类型,有时你需要比此更多的灵活性。

假设我们想要添加一个方法来获取一系列元素,并将它们全部推送到栈上:

// 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);

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

泛型提供了一种特殊的参数化类型来调用一个限定通配符类型来处理这种情况。

pushAll 的输入参数的类型不应该是「EIterable 接口」,而应该是「E 的某个子类型的 Iterable 接口」,并且有一个通配符类型,这意味着:Iterable<? extends E>

public void pushAll(Iterable<? extends E> src) {
    for (E e : src)
        push(e);
}
public void popAll(Collection<? super E> dst) {
    while (!isEmpty())
        dst.add(pop());
}

为了获得最大的灵活性,对代表生产者或消费者的输入参数使用通配符类型。

示例代码Item31Example01.java:使用限定通配符来增加 API 的灵活性。

应该使用哪种通配符类型?

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

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 助记符抓住了使用通配符类型的基本原则。

类型参数和通配符之间具有双重性

类型参数和通配符之间具有双重性,许多方法可以用一个或另一个声明。

例如,下面是两个可能的声明,用于交换列表中两个索引项目的静态方法。

// 使用无限制类型参数
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)));
}

问题是列表的类型是 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 都是消费者。