Java 教程是为 JDK 8 编写的。本页中描述的示例和实践未利用在后续版本中引入的改进。
在本节中,我们将考虑通配符的一些更高级的用法。我们已经看到了几个例子,当从数据结构中读取时,有界通配符很有用。现在考虑反向,一种只写数据结构。接口 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()
的调用是非法的,因为不能推断出有效的类型实参;String
和 Object
都不是 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 (通配符捕获),允许编译器将通配符的未知类型推断为泛型方法的类型实参。