文档

Java™ 教程-Java Tutorials 中文版
使用通配符更多的乐趣
Trail: Bonus
Lesson: Generics

使用通配符更多的乐趣

在本节中,我们将考虑通配符的一些更高级的用法。我们已经看到了几个例子,当从数据结构中读取时,有界通配符很有用。现在考虑反向,一种只写数据结构。接口 Sink 就是这种简单的例子。

interface Sink<T> {
    flush(T t);
}

我们可以想象使用它,如下面的代码所示。方法 writeAll() 旨在将集合 coll 的所有元素刷入到接收器 snk,并返回刷入的最后一个元素。

public static <T> T writeAll(Collection<T> coll, Sink<T> snk) {
    T last;
    for (T t : coll) {
        last = t;
        snk.flush(last);
    }
    return last;
}
...
Sink<Object> s;
Collection<String> cs;
String str = writeAll(cs, s); // Illegal call.

如上所述,对 writeAll() 的调用是非法的,因为不能推断出有效的类型实参;StringObject 都不是 T 的合适类型,因为 Collection 元素和 Sink 元素必须是同一类型。

我们可以通过使用通配符修改 writeAll() 的签名来修复此错误,如下所示。

public static <T> T writeAll(Collection<? extends T>, Sink<T>) {...}
...
// Call is OK, but wrong return type. 
String str = writeAll(cs, s);

调用现在是合法的,但是赋值是错误的,因为推断的返回类型是 Object,因为 T 匹配 s 的元素类型,它是 Object

解决方案是使用一种我们还没有看到的有界通配符形式:lower bound (下界) 通配符。语法 ? super T 表示 T 的超类型的未知类型(或 T 本身;请记住超类型关系是包含自己的(reflexive))。它是我们一直在使用的有界通配符的相对的,我们使用 ? extends T 表示作为 T 的子类型的未知类型。

public static <T> T writeAll(Collection<T> coll, Sink<? super T> snk) {
    ...
}
String str = writeAll(cs, s); // Yes! 

使用此语法,调用是合法的,并且根据需要推断类型为 String

现在让我们转向一个更实际的例子。java.util.TreeSet<E> 表示已排序的 E 类型的元素树。构造 TreeSet 的一种方法是将 Comparator 对象传递给构造函数。该比较器将用于根据所需的排序对 TreeSet 的元素进行排序。

TreeSet(Comparator<E> c) 

Comparator 接口基本上是:

interface Comparator<T> {
    int compare(T fst, T snd);
}

假设我们要创建一个 TreeSet<String> 并传入一个合适的比较器,我们需要传递一个 Comparator,它可以比较 String。这可以通过 Comparator<String> 完成,但 Comparator<Object> 也可以。但是,我们将无法在 Comparator<Object> 上调用上面给出的构造函数。我们可以使用下界通配符来获得我们想要的灵活性:

TreeSet(Comparator<? super E> c) 

该代码允许使用任何适用的比较器。

作为使用下界通配符的最后一个示例,让我们看一下方法 Collections.max(),它返回作为参数传递给它的集合中的最大元素。现在,为了使 max() 起作用,传入的集合的所有元素必须实现 Comparable。此外,它们必须 彼此 是比较的。

首次尝试生成此方法签名会产生:

public static <T extends Comparable<T>> T max(Collection<T> coll)

也就是说,该方法采用与自身相当的某种类型 T 的集合,并返回该类型的元素。但是,此代码过于严格。要了解原因,请考虑可与任意对象比较的类型:

class Foo implements Comparable<Object> {
    ...
}
Collection<Foo> cf = ... ;
Collections.max(cf); // Should work.

cf 的每个元素都与 cf 中的每个元素是可比的,因为每个这样的元素都是 Foo,它与任何对象都是可比的,并且特别是另一个 Foo。但是,使用上面的签名,我们发现该调用被拒绝。推断类型必须是 Foo,但 Foo 不实现 Comparable<Foo>

T 不必 正好 与本身是可比的。所需要的只是 T 与其超类型之一是可比的。这给了我们:

public static <T extends Comparable<? super T>> 
        T max(Collection<T> coll)

请注意,Collections.max() 的实际签名更复杂。我们将在下一节中回到它,Converting Legacy Code to Use Generics。这种推理适用于几乎任何用于任意类型的 Comparable 的用法:你总是想使用 Comparable<? super T>

通常,如果你的 API 仅使用类型形参 T 作为参数,则其使用应该利用下界通配符(? super T)。相反,如果 API 仅返回 T,你将通过使用上界通配符(? extends T)为你的客户提供更大的灵活性。

通配符捕获

它应该很清楚了,鉴于:

Set<?> unknownSet = new HashSet<String>();
...
/* Add an element  t to a Set s. */ 
public static <T> void addToSet(Set<T> s, T t) {
    ...
}

下面的调用是非法的。

addToSet(unknownSet, "abc"); // Illegal.

传递的实际 set 就是一个字符串 set;但重要的是,作为参数传递的表达式是未知类型 set,不能保证是字符串 set,还是任何类型的 set。

现在,请考虑以下代码:

class Collections {
    ...
    <T> public static Set<T> unmodifiableSet(Set<T> set) {
        ...
    }
}
...
Set<?> s = Collections.unmodifiableSet(unknownSet); // This works! Why?

看来这不应该被允许;但是,看看这个特定的调用,允许它是完全安全的。毕竟,unmodifiableSet() 适用于任何类型的 Set,无论其元素类型如何。

由于这种情况相对频繁地出现,因此有一个特殊规则允许在非常特定的情况下使用这些代码,在这种情况下可以证明代码是安全的。此规则称为 wildcard capture (通配符捕获),允许编译器将通配符的未知类型推断为泛型方法的类型实参。


Previous page: Class Literals as Runtime-Type Tokens
Next page: Converting Legacy Code to Use Generics