类型 Collection 表示一个未知类型 元素的集合,就像 Collection,这样说更准确。 但是等一下,那也不正确。考虑 getParts()这个调用,它返回一个 Collection。然后它被赋值给 k, 而 k 是 Collection。如果这个调用的结果是一个 Collection,这个赋值应该是一个错误。 事实上,这个赋值是合法的,但是它产生一个未检查警告(unchecked warning)。这个警告是必要的, 因为事实是编译器无法保证其正确性。我们没有办法检查 getAssembly()中的旧代码来保证返回的确实是 一个 Collection。代码里使用的类型是 Collection,可以合法的向其中加入任何 Object。 那么,这应该是一个错误么?理论上讲,Yes,但是实际上讲,如果泛型代码要调用旧代码,那么这必 须被允许。这取决于你,程序员,在这种情况下来满足你自己。这个赋值是合法的因为 getAssembly()的 调用约定中说它返回一个 Part 的集合,即使这个类型声明中没有显示出这一点。 因此,自然类型和通配符类型很像,但是他们的类型检查不是同样严格。允许泛型与已经存在的老代 码相交互是一个深思熟虑的决定。 从泛型代码中调用老代码具有先天的危险性,一旦你把泛型编程和非泛型编程混合起来,泛型系统所 提供的所有安全保证都失效。然而,你还是比你根本不用泛型要好。至少你知道你这一端的代码是稳定的。 在非泛型代码远比泛型代码多的时候,不可避免会出现两者必须混合的情况。 如果你发现你不得不混合旧代码和泛型代码,仔细注意未检查警告(unchecked warnings),仔细考虑 你怎样才能证明出现警告的部分代码是正确的。 如果你仍然犯了错,而导致警告的代码确实不是类型安全的,那么会发生什么?让我们看一下这种情 形。在这个过程中,我们将了解一些编译器工作的内幕。 6.2. 擦除和翻译 擦除和翻译(Erasure and Translation) public class Test { public static String loophole(Integer x) { List ys = new LinkedList(); List xs = ys; xs.add(x); // compile-time unchecked warning return ys.iterator().next(); } public static void main(String[] args) { // 运行时抛出 ClassCastException String s =loophole(2); System.out.println(s); } } 这里,我们用一个老的普通的 list 的引用来指向一个 String 的 list。我们插入一个 Integer 到这个 list 中,并且试图得到一个 String。这是明显的错误。如果我们忽略这 个警告并且试图运行以上代码,它将在我们试图使用错误的类型的地方失败。在运行的时 候,上面的代码与下面的代码的行为一样: public String loophole(Integer x) { List ys = new LinkedList(); List xs = ys; xs.add(x); return (String) ys.iterator().next(); // run time error } 当我们从 list 中获取一个元素的时候,并且试图通过转换为 String 而把它当作一个 string,我们得到一个 ClassCastException。完全一样的事情发生在使用泛型的代码上。 这样的原因是,泛型是通过 java 编译器的称为擦除(erasure)的前端处理来实现的。 你可以(基本上就是)把它认为是一个从源码到源码的转换,它把泛型版本的 loophole() 转换成非泛型版本。 结果是,java 虚拟机的类型安全和稳定性决不能冒险,即使在又 unchecked warning 的情况下。 基本上, 擦除去掉了所有的泛型类型信息。 所有在尖括号之间的类型信息都被扔掉了, 因此,比如说一个 List类型被转换为 List。所有对类型变量的引用被替换成类 型变量的上限(通常是 Object)。而且,无论何时如果结果代码类型不正确,会插入一个到 合适的类型的转换,就像 loophole 的最后一行那样。 擦除的全部的细节超出了本文的范围,但是我们给出的简单描述与事实很接近。知道 一点这个有好处,特别是如果你要作一些复杂的事,比如把现有 API 转换成使用泛型的代 码(第 10 部分)或者仅仅是想理解为什么会这样。 6.3. 在老代码中使用泛型代码 现在让我们来考虑相反的情形。假定 Fooblibar.com 公司的人决定把他们的代码转换为使用泛型来实 现,但是他们的一些客户没有转换。现在代码就像下面: package com.Fooblibar.widgets; public interface Part { ...} //-------------------------------------------------------------public class Inventory { public static void addAssembly(String name, Collection parts) {//…} public static Assembly getAssembly(String name) {...} } //-------------------------------------------------------------public interface Assembly { Collection getParts(); // Returns a collection of Parts } //-------------------------------------------------------------//客户端代码如下: package com.mycompany.inventory; import com.Fooblibar.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()); Inventory.addAssembly(”thingee”, c); // 1: unchecked warning Collection k = Inventory.getAssembly(”thingee”).getParts(); } } 客户端代码是在泛型被引入之前完成的,但是它使用了包 com.Fooblibar.widgets 和集合库,它们都 使用了泛型。 客户端代码中的泛型类的声明都是使用了自然类型(raw types)。 1 行产生一个 unchecked 第 warning,因为一个自然的 Collection 被传递到一个需要 Collection的地方,而编译器无法保证 Collection 就是一个 Collection。 你还有另一种选择,你可以使用 source 1.4 标志来编译客户端代码,以保证不会产生警告。但是这种 情况下你无法使用 jdk1.5 中的任何新特性。 7. 要点 要点(The Fine Print) 7.1. 一个泛型类被其所有调用共享 下面的代码打印的结果是什么? List l1 = new ArrayList(); List l2 = new ArrayList(); System.out.println(l1.getClass() == l2.getClass()); 或许你会说 false,但是那你就错了。它打印出 true。因为所有的泛型类型在运行时 有同样的类(class),就是说:所有的泛型在运行时共享一个类,而不管他们的实际类型参 数。 事实上,泛型之所以为泛型就是因为它对所有其可能的类型参数,它有同样的行为; 同样的类可以被当作许多不同的类型。 7.2. 转型和 instanceof 泛型类被所有其实例(instances)共享的另一个暗示是检查一个实例是不是一个特定类型的泛型类是 检查一个实例是不是一个特定类型的泛型类是 没有意义的。 没有意义的。 Collection cs = new ArrayList(); if (cs instanceof Collection) { ...} // 非法 类似的,如下的类型转换 Collection cstr = (Collection) cs; 得到一个 unchecked warning,因为运行时环境不会为你作这样的检查。 类型参数在运行时并不存在,而只在编译时期存在。这意味着它们不会添加任何的时间或者空间上的负 类型参数在运行时并不存在,而只在编译时期存在。 担,这很好。不幸的是,这也意味着你不能依靠他们进行类型转换。 8. More fun with * 在这一部分,我们来考虑一些通配符得高级用法。我们已经看到了上限通配符在从一 个数据结构中进行读取的几个例子。现在考虑相反的情况,一个只写的数据结构。 接口 Sink 是这种情况的一个简单例子。 interface Sink { void flush(T t); } 我们可以想象他被如下面的代码一样使用。