Java 教程是为 JDK 8 编写的。本页中描述的示例和实践未利用在后续版本中引入的改进。
到目前为止,我们所有的例子都假设了一个理想化的世界,每个人都在使用最新版本的 Java 编程语言,它支持泛型。
唉,实际情况并非如此。在早期版本的语言中已经编写了数百万行代码,并且它们不会在一夜之间全部转换。
稍后,在 Converting Legacy Code to Use Generics 部分中,我们将解决将旧代码转换为使用泛型的问题。在本节中,我们将关注一个更简单的问题:遗留代码和泛型代码如何互操作?这个问题有两个部分:在泛型代码中使用遗留代码和在遗留代码中使用泛型代码。
如何使用旧代码,同时仍然在自己的代码中享受泛型的好处?
例如,假设你要使用包 com.Example.widgets
。Example.com 的人们推出了一个库存控制系统,其亮点如下所示:
package com.Example.widgets; public interface Part {...} public class Inventory { /** * Adds a new Assembly to the inventory database. * The assembly is given the name name, and * consists of a set parts specified by parts. * All elements of the collection parts * must support the Part interface. **/ public static void addAssembly(String name, Collection parts) {...} public static Assembly getAssembly(String name) {...} } public interface Assembly { // Returns a collection of 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
的泛型类型而没有类型形参时,它被称为 raw type (原始类型)。
大多数人的第一直觉是 Collection
实际上意味着 Collection<Object>
。但是,正如我们之前看到的那样,在需要 Collection<Object>
的地方传递 Collection<Part>
是不安全的。更确切地说,Collection
类型表示某种未知类型的集合,就像 Collection<?>
一样。
但等等,这也不是正确的!考虑调用 getParts()
,它返回 Collection
。然后将其分配给 k
,这是 Collection<Part>
。如果调用的结果是 Collection<?>
,则赋值将会出错。
实际上,分配是合法的,但它会生成 unchecked warning (未经检查的警告)。这个警告是必要的,因为事实是编译器无法保证其正确性。我们无法检查 getAssembly()
中的遗留代码,以确保返回的集合确实是 Part
的集合。代码中使用的类型是 Collection
,可以合法地将所有类型的对象插入到这样的集合中。
那么,这不应该是一个错误吗?从理论上讲,是的;但实际上,如果泛型代码要调用遗留代码,则必须允许这样做。程序员可以自己确定,在这种情况下,赋值是安全的,因为 getAssembly()
的契约表示它返回 Part
的集合,即使类型签名没有显示这一点。
因此原始类型非常类似于通配符类型,但它们并没有严格地进行类型检查。这是一个深思熟虑的设计决策,允许泛型与预先存在的遗留代码进行互操作。
从泛型代码调用遗留代码本质上是危险的;一旦将泛型代码与非泛型遗留代码混合,泛型类型系统通常提供的所有安全保证都是无效的。但是,你仍然比没有使用泛型更好。至少你知道你的代码是一致的。
目前有非常多的非泛型代码,然后有泛型代码,并且不可避免地会出现需要混合的情况。
如果你发现必须混合使用旧代码和泛型代码,请密切注意未经检查的警告。仔细考虑如何证明产生警告的代码的安全性。
如果你仍然犯了错误会发生什么,导致警告的代码确实不是类型安全的?我们来看看这种情况。在此过程中,我们将深入了解编译器的工作原理。
public String loophole(Integer x) { List<String> ys = new LinkedList<String>(); List xs = ys; xs.add(x); // Compile-time unchecked warning 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(); // run time error }
当我们从列表中提取元素并尝试通过将其转换为 String
将其视为字符串时,我们将得到 ClassCastException
。与 loophole()
的泛型版本完全相同。
原因是,Java 编译器将泛型实现为名为 erasure (擦除) 的前端转换。你可以(几乎)将其视为源到源的转换,其中 loophole()
的泛型版本将转换为非泛型版本。
因此, 即使存在未经检查的警告,Java 虚拟机的类型安全性和完整性也不会存在风险。
基本上,擦除消除了(或 erases (擦除))所有泛型类型信息。抛弃尖括号之间的所有类型信息,例如,像 List<String>
这样的参数化类型被转换为 List
。类型变量的所有剩余使用都由类型变量的上界替换(通常为 Object
)。并且,只要结果代码不是类型正确的,就会插入适当类型的强制转换,如 loophole
的最后一行。
擦除的全部细节超出了本教程的范围,但我们刚刚给出的简单描述与事实相差无几。了解一点这一点很好,特别是如果你想做更复杂的事情,比如将现有的 API 转换为使用泛型(参见 Converting Legacy Code to Use Generics 部分),或者只是想了解为什么事情是这样的。
现在让我们考虑相反的情况。想象一下,Example.com 选择将他们的 API 转换为使用泛型,但他们的一些客户端还没有。所以现在代码看起来像:
package com.Example.widgets; public interface Part { ... } public class Inventory { /** * Adds a new Assembly to the inventory database. * The assembly is given the name name, and * consists of a set parts specified by parts. * All elements of the collection parts * must support the Part interface. **/ public static void addAssembly(String name, Collection<Part> parts) {...} public static Assembly getAssembly(String name) {...} } public interface Assembly { // Returns a collection of Parts Collection<Part> getParts(); }
客户端代码如下所示:
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 c = new ArrayList(); c.add(new Guillotine()) ; c.add(new Blade()); // 1: unchecked warning 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 中引入的任何新语言功能。