接上篇博文
《Java范型那些事(一)》
参考oracle官网对于范型的介绍 : https://docs.oracle.com/javase/tutorial/extra/generics/legacy.html
目录
9. 使用通配符?的更多乐趣
通配符捕获
10. 使用范型改写旧代码(未使用范型的代码)
9. 使用通配符?的更多乐趣
在这一节,我们将考虑通配符的更多高级用法。在前面几节的介绍中,我们已经看到了几个例子,当从数据结构中读取时,有界通配符很有用。现在考虑相反的情况,一种只写数据结构。接口Sink就是这种简单的例子。
interface Sink<T> {
flush(T t);
}
我们可以想象通过如下面的代码所示来使用它。方法writeAll()旨在将集合coll的所有元素刷新到sink 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>) {...}
...
// 调用正常,但返回类型错误
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对象传递给构造函数。该比较器Comparator将用于根据所需的顺序对TreeSet的元素进行排序。
TreeSet(Comparator<E> c)
Comparator接口实质上是:
interface Comparator<T> {
int compare(T fst, T snd);
}
假设我们想要创建一个TreeSet <String>并传入一个合适的比较器,我们需要传递一个可以比较Strings的Comparator。这可以通过Comparator <String>完成,但Comparator <Object>也可以。但是,我们将无法在Comparator <Object>上调用上面给出的构造函数。我们可以使用较低的有界通配符来获得我们想要的灵活性
TreeSet(Comparator<? super E> c)
以上代码允许使用任何适用的Comparator。
作为使用下边界通配符的最后一个示例,让我们看一下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并没有实现Comparable <Foo>。
T不必与其本身相比较。所需要的只是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)为您的客户提供更大的灵活性。
通配符捕获
现在应该很清楚,给定:
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.
传递的实际集合是一组字符串没有区别;重要的是,作为参数传递的表达式是一组未知类型,不能保证是一组字符串,特别是任何类型的字符串。
现在,请考虑以下代码:
class Collections {
...
<T> public static Set<T> unmodifiableSet(Set<T> set) {
...
}
}
...
Set<?> s = Collections.unmodifiableSet(unknownSet); // This works! Why?
这似乎不应该被允许;但是,看看这个特定的调用,允许它是完全安全的。毕竟,unmodifiableSet()适用于任何类型的Set,无论其元素类型如何。
因为这种情况相对频繁地出现,所以有一个特殊的规则允许在非常特定的情况下使用这些代码,在这种情况下代码可以被证明是安全的。此规则称为通配符捕获,允许编译器将通配符的未知类型推断为泛型方法的类型参数。
10. 使用范型改写旧代码(未使用范型的代码)
之前,我们展示了新旧代码如何互操作。 现在,是时候看看“泛化”旧代码的难题了。
如果您决定将旧代码转换为使用泛型,则需要仔细考虑如何修改API。
您需要确保修改后的范型API不会有过度的限制; 它必须继续支持API的原始约定。 再次考虑java.util.Collection中的一些示例。 没有使用范型之前的API如下所示:
interface Collection {
public boolean containsAll(Collection c);
public boolean addAll(Collection c);
}
初次尝试使用范型改写后,可能会像下面这样:
interface Collection<E> {
public boolean containsAll(Collection<E> c);
public boolean addAll(Collection<E> c);
}
虽然这显然是类型安全的,但也符合API的原始规定。 containsAll()方法适用于任何类型的传入集合。只有当传入的集合实际上只包含E的实例时,它才会成功,但是:
- 传入集合的静态类型可能不同,可能是因为调用者不知道传入的集合的精确类型,或者可能是因为它是Collection <S>,其中S是E的子类型。
- 使用一个不同类型的集合作为参数,传入containsAll()是完全合法的。按照期望应该能调用,然后返回false。
在addAll()的情况下,我们应该能够添加由E子类型的实例组成的任何集合。我们在范型方法部分中看到了如何正确处理这种情况。
您还需要确保修订后的API保留与旧客户端的代码兼容性。这意味着API的擦除范型类型后,必须与原始的、未使用范型的API相同。在大多数情况下,这自然会失败,但有一些微妙的情况。我们将研究我们遇到过的最微妙的案例之一,即Collections.max()方法。正如我们在第9节,使用通配符更多乐趣部分中看到的,max()的一个合理签名是:
public static <T extends Comparable<? super T>>
T max(Collection<T> coll)
代码看上去很好,除了在擦除范型类型后,是这样的:
public static Comparable max(Collection coll)
但这与max()的原始签名不同:
public static Object max(Collection coll)
当然可以为max()指定此签名,但是没有完成,并且调用Collections.max()的所有旧类文件都依赖于返回Object的签名。
我们可以通过在形式类型参数T的边界中显式指定一个超类,来强制使得类型擦除跟上面的不一样。
public static <T extends Object & Comparable<? super T>>
T max(Collection<T> coll)
这是使用语法T1和T2 ...&Tn为类型参数赋予多个边界的示例。 具有多个边界的类型变量,是边界中列出的所有类型的子类型。 当使用多边界时,边界中提到的第一种类型用作类型变量的擦除。
最后,我们应该记得max只读取其输入集合,因此适用于T的任何子类型的集合。
这将我们带到JDK中使用的实际签名:
public static <T extends Object & Comparable<? super T>>
T max(Collection<? extends T> coll)
在实践中出现任何如此复杂的事情是非常罕见的,但专业library设计师应该在使用范型转换现有API时仔细考虑。
需要注意的另一个问题是协变返回,即改进子类中方法的返回类型。 您不应该在旧API中利用此功能。 为了了解原因,让我们看一个例子。
假设您的原始API具有以下形式:
public class Foo {
// Factory. Should create an instance of
// whatever class it is declared in.
public Foo create() {
...
}
}
public class Bar extends Foo {
// Actually creates a Bar.
public Foo create() {
...
}
}
为了利用协变返回的优势,你可以将它修改为:
public class Foo {
// Factory. Should create an instance of
// whatever class it is declared in.
public Foo create() {
...
}
}
public class Bar extends Foo {
// Actually creates a Bar.
public Bar create() {
...
}
}
现在假设你代码中的一个第三方客户端编写了一下代码:
public class Baz extends Bar {
// Actually creates a Baz.
public Foo create() {
...
}
}
Java虚拟机不直接支持覆盖具有不同返回类型的方法。 此功能是由编译器来支持。 因此,除非重新编译Baz类,否则它将无法正确覆盖Bar的create()方法。 此外,Baz必须被修改,因为代码将被写入拒绝 - Baz中的create()的返回类型不是Bar中返回类型create()的子类型。
更多关于Java的协变、逆变和不变的介绍,可以参考我的另外一篇博客:Kotlin笔记12-Java和Kotlin中的范型对比(一)