这些Java教程是针对JDK 8编写的。本页面中描述的示例和实践不利用后续版本中引入的改进,并且可能使用已不再可用的技术。
有关Java SE 9及其后续版本中更新的语言特性的摘要,请参阅Java语言更改。
有关所有JDK版本的新功能、增强功能以及已删除或不推荐使用的选项的信息,请参阅JDK发布说明。
之前,我们展示了新旧代码如何互相操作。现在,是时候来看看更难的问题,即“泛型化”旧代码。
如果你决定将旧代码转换为使用泛型,你需要仔细考虑如何修改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()
。正如我们在更多关于通配符的有趣内容中看到的,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)
在实践中很少遇到如此复杂的情况,但专业的库设计者在转换现有API时应该仔细思考。
另一个要注意的问题是协变返回,即在子类中细化方法的返回类型。你不应该在旧API中利用此特性。为了理解原因,让我们看一个例子。
假设你的原始API的形式如下:
public class Foo { // 工厂。应该创建它所声明的类的实例。 public Foo create() { ... } } public class Bar extends Foo { // 实际创建一个 Bar。 public Foo create() { ... } }
利用协变返回,你将其修改为:
public class Foo { // 工厂。应该创建它所声明的类的实例。 public Foo create() { ... } } public class Bar extends Foo { // 实际创建一个 Bar。 public Bar create() { ... } }
现在,假设你的代码的第三方客户端编写了以下内容:
public class Baz extends Bar { // 实际创建一个 Baz。 public Foo create() { ... } }
Java虚拟机不直接支持具有不同返回类型的方法覆盖。这个特性由编译器支持。因此,除非重新编译类 Baz
,否则它不会正确地覆盖 Bar
中的 create()
方法。此外,Baz
必须被修改,因为代码将被拒绝,写在 Baz
中的 create()
的返回类型不是 Bar
中 create()
的返回类型的子类型。