该Java教程是针对JDK 8编写的。本页面中描述的示例和实践不利用后续版本中引入的改进,并可能使用不再可用的技术。
请参阅Java语言更改,了解Java SE 9及后续版本中更新的语言特性的摘要。
请参阅JDK发布说明,了解所有JDK版本的新功能、增强功能以及已删除或已弃用选项的信息。
到目前为止,我们所有的例子都假设了一个理想化的世界,每个人都在使用支持泛型的最新版本的Java编程语言。
然而,现实情况并非如此。已经有数百万行的代码是用早期版本的语言编写的,而且不可能一夜之间全部转换。
稍后,在将旧代码转换为使用泛型一节中,我们将解决将旧代码转换为使用泛型的问题。在本节中,我们将专注于一个更简单的问题:如何使旧代码和泛型代码可以互操作?这个问题有两个部分:在泛型代码中使用旧代码和在旧代码中使用泛型代码。
在你自己的代码中如何使用旧代码,同时又享受泛型带来的好处呢?
以一个例子来说明,假设你想使用com.Example.widgets
这个包。Example.com的人们市场上有一个用于库存控制的系统,其中一些亮点如下:
package com.Example.widgets; public interface Part {...} public class Inventory { /** * 向库存数据库中添加新的组件。 * 组件的名称为name,由parts指定。 * parts集合中的所有元素必须支持Part接口。 **/ public static void addAssembly(String name, Collection parts) {...} public static Assembly getAssembly(String name) {...} } public interface Assembly { // 返回一个Parts的集合 Collection getParts(); }
现在,你想添加一个使用上述API的新代码。最好能确保你总是以正确的参数调用addAssembly()
,也就是说,传递给它的集合确实是Part
类型的Collection
。当然,泛型就是为此而生:
package com.mycompany.inventory; import com.Example.widgets.*; public class Blade implements Part { ... } public class Guillotine implements Part { } public class Main { public static void main(String[] args) { Collection<Part> c = new ArrayList<Part>(); c.add(new Guillotine()) ; c.add(new Blade()); Inventory.addAssembly("thingee", c); Collection<Part> k = Inventory.getAssembly("thingee").getParts(); } }
当我们调用addAssembly
时,它期望第二个参数的类型是Collection
。而实际的参数是Collection<Part>
类型。这样是行得通的,但为什么呢?毕竟,大多数集合不包含Part
对象,所以一般来说,编译器无法知道类型Collection
指的是什么类型的集合。
在合适的泛型代码中,Collection
总是伴随着一个类型参数。当一个泛型类型像 Collection
在没有类型参数的情况下使用时,它被称为 原始类型。
大多数人的第一直觉是 Collection
实际上意味着 Collection<Object>
。然而,正如我们之前看到的,将一个 Collection<Part>
传递给需要 Collection<Object>
的地方是不安全的。更准确地说,类型 Collection
表示一种未知类型的集合,就像 Collection<?>
。
但是等等,这也不对!考虑对 getParts()
的调用,它返回一个 Collection
。然后将其赋值给 k
,它是一个 Collection<Part>
。如果调用的结果是 Collection<?>
,赋值将会出错。
实际上,这个赋值是合法的,但是它会生成一个 未经检查的警告。这个警告是必要的,因为事实是编译器无法保证其正确性。我们无法检查 getAssembly()
中的遗留代码,以确保返回的确实是 Part
的集合。代码中使用的类型是 Collection
,人们可以合法地将各种对象插入这样的集合中。
那么,这不应该是一个错误吗?理论上来说,是的;但实际上,如果泛型代码要调用遗留代码,这是必须允许的。由你作为程序员来满足自己,在这种情况下,赋值是安全的,因为 getAssembly()
的契约规定它返回一种 Part
的集合,尽管类型签名没有显示这一点。
因此,原始类型与通配符类型非常相似,但它们的类型检查没有那么严格。这是一个故意的设计决策,允许泛型与现有的遗留代码互操作。
从泛型代码调用遗留代码本质上是危险的;一旦将泛型代码与非泛型遗留代码混合使用,通常提供的泛型类型系统的所有安全保证都将失效。然而,相比不使用泛型,你仍然更好一些。至少你知道自己的代码是一致的。
目前,非泛型代码远远超过泛型代码,不可避免地会出现它们必须混合使用的情况。
如果发现必须混合使用遗留代码和泛型代码,请密切关注未经检查的警告。仔细思考如何证明引发警告的代码的安全性。
如果你仍然犯了一个错误,导致警告的代码确实不安全,会发生什么呢?让我们看看这样的情况。在这个过程中,我们将对编译器的工作原理有一些了解。
public String loophole(Integer x) { List<String> ys = new LinkedList<String>(); List xs = ys; xs.add(x); // 编译时未检查警告 return ys.iterator().next(); }
在这里,我们给字符串列表和普通列表起了一个别名。我们将一个Integer
插入到列表中,并尝试提取一个String
。这显然是错误的。如果我们忽略警告并尝试执行这段代码,它将在我们尝试使用错误类型的地方失败。在运行时,这段代码的行为如下:
public String loophole(Integer x) { List ys = new LinkedList; List xs = ys; xs.add(x); return(String) ys.iterator().next(); // 运行时错误 }
当我们从列表中提取一个元素,并尝试将其强制转换为String
时,我们将得到一个ClassCastException
。与泛型版本的loophole()
发生的事情完全相同。
之所以会这样,是因为泛型在Java编译器中是通过一种称为擦除的前端转换来实现的。你可以(几乎)把它看作是一种源代码到源代码的转换,其中泛型版本的loophole()
被转换为非泛型版本。
结果是,即使存在未检查的警告,Java虚拟机的类型安全性和完整性也不会受到威胁。
基本上,擦除会去掉(或擦除)所有的泛型类型信息。所有的尖括号之间的类型信息都被丢弃,所以,例如,一个参数化类型如List<String>
被转换为List
。所有剩余的类型变量的使用都被替换为类型变量的上界(通常是Object
)。并且,每当生成的代码不符合类型规范时,会插入一个适当类型的强制转换,就像loophole
的最后一行那样。
关于擦除的详细信息超出了本教程的范围,但我们刚刚给出的简单描述并不远离真相。了解一些关于这个的知识是很好的,特别是如果你想要做一些更复杂的事情,比如将现有的API转换为使用泛型(参见将遗留代码转换为使用泛型部分),或者只是想要理解为什么事情会变成现在这个样子。
现在让我们考虑相反的情况。假设Example.com选择将他们的API转换为使用泛型,但其中一些客户端还没有这样做。所以现在代码看起来像:
package com.Example.widgets; public interface Part { ... } public class Inventory { /** * 将一个新的组件添加到库存数据库中。 * 组件的名称是name,并且由parts指定的一组零件组成。 * 集合parts的所有元素必须支持Part接口。 **/ public static void addAssembly(String name, Collection<Part> parts) {...} public static Assembly getAssembly(String name) {...} } public interface Assembly { // 返回一组零件 Collection<Part> getParts(); }
客户端代码如下:
包 com.mycompany.inventory; 导入 com.Example.widgets.*; public class Blade implements Part { ... } public class Guillotine implements Part { } public class Main { public static void main(String[] args) { Collection c = new ArrayList(); c.add(new Guillotine()) ; c.add(new Blade()); // 1:未检查的警告 Inventory.addAssembly("thingee", c); Collection k = Inventory.getAssembly("thingee").getParts(); } }
客户端代码是在引入泛型之前编写的,但它使用了com.Example.widgets
包和集合库,两者都使用了泛型类型。客户端代码中所有的泛型类型声明都是原始类型。
第1行生成了一个未检查的警告,因为正在传递一个原始的Collection
,而期望的是一个Part
的Collection
,编译器无法确保原始的Collection
真的是Part
的Collection
。
作为一种替代方案,您可以使用source 1.4标志编译客户端代码,以确保不会生成任何警告。然而,在这种情况下,您将无法使用JDK 5.0引入的任何新语言特性。