<< 类字面量作为运行时类型标记 · 目录 · 将老式代码转换为使用泛型 >>
通配符的更多用法 (More Fun with Wildcards)
在本节中,我们将考虑通配符的一些更高级的用途。我们已经看到了几个例子,其中有界通配符在从数据结构读取数据时非常有用。现在考虑相反的情况,即只写数据结构。接口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); // 非法调用
正如所写的,对writeAll()的调用是非法的,因为编译器不能推断有效的类型参数;String和Object都不是T的适当类型,因为Collection的元素和Sink的元素必须是相同的类型。
我们可以通过修改writeAll()的签名来修复这个错误,如下所示,使用通配符:
public static <T> T writeAll(Collection<? extends T>, Sink<T>) {...}
...
// 调用没有问题,但是返回类型错误
String str = writeAll(cs, s);
调用现在是合法的,但是赋值是错误的,因为推断的返回类型是Object,因为T匹配s的元素类型,即Object。
解决方案是使用一种我们尚未见过的有界通配符形式:有下界的通配符。语法 ? Super T 表示未知类型?是T的一个超类型(或T本身;请记住,超类型关系是自反的)。它是我们一直使用的有界通配符的对偶,使用 ? 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)
比较器接口大体如下:
interface Comparator<T> {
int compare(T fst, T snd);
}
假设我们想要创建一个TreeSet<String>并传入一个合适的比较器,我们需要传递一个Comparator来比较字符串。这可以由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能与它的某个超类型相比较。这样:
public static <T extends Comparable<? super T>> T max(Collection<T> coll)
注意,Collections.max()的实际签名更加复杂。在下一节将老式代码转换为使用泛型中,我们将再次见到它。这种推理几乎适用于适合任意类型的Comparable用法:您总是希望使用Comparable<? super T>。
通常,如果您的API只使用类型参数T作为参数,那么它的使用应该利用下界通配符(? super T)。相反,如果API只返回T,则通过使用上限通配符(? extends T)。
通配符捕获 (wildcard capture)
现在应该很清楚了,如下:
Set<?> unknownSet = new HashSet<String>();
...
/* 添加一个元素 t 到一个 Set s. */
public static <T> void addToSet(Set<T> s, T t) {
...
}
下面是非法调用:
addToSet(unknownSet, "abc"); // Illegal.
传递的实际集合是一组字符串,这一点没有区别;重要的是,参数传递的是一组未知类型的集合,不能保证它是一组字符串,特别是任意类型。
现在,考虑以下代码:
class Collections {
...
<T> public static Set<T> unmodifiableSet(Set<T> set) {
...
}
}
...
Set<?> s = Collections.unmodifiableSet(unknownSet); // 可以正常运行?为什么?
这似乎是不允许的;然而,看看这个特定的调用,允许它是完全安全的。毕竟,unedfiableSet()确实适用于任何类型的集合,而不管它的元素类型如何。
由于这种情况出现的相对频繁,因此有一个特殊的规则,允许在可以证明代码是安全的特定环境下使用这种代码。这个规则称为通配符捕获,允许编译器将未知类型的通配符推断为泛型方法的类型参数。
<< 类字面量作为运行时类型标记 · 目录 · 将老式代码转换为使用泛型 >>
本文译自:https://docs.oracle.com/javase/tutorial/extra/generics/morefun.html