文档

Java™教程
隐藏目录
更多通配符的乐趣
路径:奖励
课程:泛型

更多关于通配符的乐趣

在这个部分,我们将考虑一些更高级的通配符使用方法。我们已经看到了一些有界通配符在从数据结构中读取时的有用性的示例。现在考虑相反的情况,一个只写的数据结构。接口Sink是一个简单的例子。

接口 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); // 是的! 

使用这种语法,调用是合法的,并且推断的类型是String,正如所期望的那样。

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

TreeSet(Comparator<E> c) 

Comparator接口的本质是:

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

假设我们想要创建一个TreeSet<String>并传入一个合适的比较器,我们需要传入一个可以比较StringComparator。这可以通过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); // 应该可以工作。

cf的每个元素都可以与cf中的其他元素进行比较,因为每个元素都是Foo,它可以与任意对象进行比较,特别是与另一个Foo进行比较。然而,使用上面的方法签名,我们发现该调用被拒绝。推断的类型必须是Foo,但是Foo没有实现Comparable<Foo>

T不必与exactly自身进行比较。只需要T与其任何超类型可比较。这给了我们:

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

请注意,Collections.max()的实际签名更复杂。我们将在下一节“将遗留代码转换为使用泛型”中回到它。这种推理适用于几乎任何希望适用于任意类型的Comparable的用法:您总是希望使用Comparable<? super T>

通常情况下,如果你有一个只使用类型参数T作为参数的API,它的使用应该利用下界通配符(? super T)。相反,如果API只返回T,通过使用上界通配符(? extends T),你可以给客户端更大的灵活性。

通配符捕获

现在应该很清楚了,给定以下代码:

Set<?> unknownSet = new HashSet<String>();
...
/* 向Set s中添加元素 t。 */ 
public static <T> void addToSet(Set<T> s, T t) {
    ...
}

下面的调用是非法的。

addToSet(unknownSet, "abc"); // 非法。

实际传递的集合是字符串集合并没有关系,关键是作为参数传递的表达式是一个未知类型的集合,不能保证它是字符串集合,或者是特定类型的集合。

现在,考虑以下代码:

class Collections {
    ...
    <T> public static Set<T> unmodifiableSet(Set<T> set) {
        ...
    }
}
...
Set<?> s = Collections.unmodifiableSet(unknownSet); // 这是可以的!为什么?

这似乎不应该被允许,然而,从这个具体的调用来看,允许它是完全安全的。毕竟,unmodifiableSet()对于任何类型的Set都适用,无论其元素类型是什么。

由于这种情况相对频繁,有一条特殊规则允许在非常具体的情况下使用这样的代码,其中可以证明代码是安全的。这个规则称为通配符捕获,允许编译器将通配符的未知类型推断为泛型方法的类型参数。


上一页: 类文字作为运行时类型令牌
下一页: 将遗留代码转换为使用泛型