与老式代码交互 (Interoperating with Legacy Code)
到目前为止,我们所有的例子都假设在一个理想化的世界,其中每个人都在使用支持泛型的最新JAVA编程语言。
可惜,事实并非如此。在该语言的早期版本中已经编写了数百万行代码,它们不会在一夜之间全部转换。
稍后,在 转换老式代码为使用泛型 一节中,我们将处理将老式代码转换为使用泛型的问题。在本节,我们将关注一个更简单的问题:老式代码与泛型代码怎样交互?这个问题有两个部分:在泛型代码中使用老式代码和在老式代码中使用泛型代码。
在泛型代码中使用老式代码
如何使用老式代码,同时仍能在自己的代码中享受泛型的好处?
举个例子,假设您想要使用com.Example.widgets包。在Example.com上,人们销售一种库存控制系统,其要点如下所示:
package com.Example.widgets;
public interface Part {...}
public class Inventory {
/**
* 向库存数据库添加一个新的零件组合Assembly
* 该零件组合给定名称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集合。当然,泛型是为此量身定做的:
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)而没有类型参数时,它被称为原始类型(raw type)。
大多数人的第一反应是,Collection实际上意味着Collection<Object>。但是,正如我们前面看到的,在需要Collection<Object>的地方传递Collection<Part>是不安全的。更准确的说法是,Collection类型表示某种未知类型的集合,就像Collection<?>一样。
但是等等,那也不太对!考虑对getParts()的调用,它返回一个Collection。然后将其分配给k,是一个Collection<Part>类型。如果调用的结果是Collection<?>,则分配将是一个错误。
实际上,分配是合法的,但它会生成未经检查的警告(unchecked warning)。这个警告很有必要,因为编译器不能保证其正确性。我们无法检查getAssembly()中的老式代码,以确保返回的集合确实是部件的集合。代码中使用的类型是Collection,你可以合法地将所有类型的对象插入到这样的集合中。
所以,这不应该是个错误吗?理论上讲是的,但实际上,如果泛型代码要调用老式代码,则必须允许这样做。由程序员自己决定,在这种情况下,分配是安全的,因为getAssembly()的契约说它返回部件集合,尽管类型签名没有显示这一点。
所以原始类型非常类似于通配符类型,但是它们没有严格地进行类型检查。这是一个经过深思熟虑的设计决策,允许泛型与已有的老式代码进行交互。
从泛型代码调用老式代码本质上是危险的;一旦将泛型代码与非泛型老式代码混合,泛型类型系统通常提供的所有安全保障都是无效的。但是,仍然比完全不使用泛型要更好,至少你知道你那里的代码是一致的。
目前非泛型代码比泛型代码多得多,而且不可避免地会出现它们必须混合的情况。
如果发现必须混合老式代码和泛型代码,请密切注意未经检查的警告。仔细考虑如何证明引起警告的代码的安全性。
如果您仍然犯了一个错误,并且导致警告的代码确实不安全,那么会发生什么情况呢?让我们来看看这种情况。在此过程中,我们将些许深入了解编译器的工作原理。
擦除和翻译
public String loophole(Integer x) {
List<String> ys = new LinkedList<String>();
List xs = ys;
xs.add(x); // 编译时 uncheck 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)的前端转换。您可以(几乎)将其视为源到源(source-to-source)的转换,从而将泛型版本的loophole()转换为非泛型版本。
因此,即使存在未经检查的警告,Java虚拟机的类型安全性和完整性也不会受到威胁。
基本上,擦除会消除所有泛型类型信息。所有的尖括号之间的类型信息都被抛弃,因此,例如,将List<String>这样的参数化类型转换为List。类型变量的所有剩余用途都被替换成类型变量的上界(通常是Object)。而且,每当生成的代码类型不正确时,就会插入合适的类型转换,就像loophole的最后一行一样。
“擦除”的全部细节超出了本教程的范围,但我们刚才给出的简单描述与事实不相上下。了解一下这一点很好,特别是如果您想做更复杂的事情,比如转换现有的API以使用泛型(请参阅 转换老式代码为使用泛型 小节),或者只是想了解为什么事情是这样的。
在老式代码中使用泛型
现在我们来考虑相反的情况。假设Example.com选择将他们的API转换为使用泛型,但是他们的一些客户还没有。所以现在代码看起来是:
package com.Example.widgets;
public interface Part {
...
}
public class Inventory {
/**
* 向库存数据库添加一个新的零件组合Assembly
* 该零件组合给定名称name,并由零件指定的一组零件parts组成。
* 所有的parts集合元素必须支持Part接口
**/
public static void addAssembly(String name, Collection<Part> parts) {...}
public static Assembly getAssembly(String name) {...}
}
public interface Assembly {
// 返回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.Examp e.widget包和集合库,这两个库都使用泛型类型。客户端代码中所有使用泛型类型声明的都是原始类型。
第1行生成未经检查的警告,因为在需要Part集合的地方传递了原始集合,而且编译器不能确保原始集合确实是Part集合。
另一种选择是,您可以使用源1.4标志编译客户机代码,以确保不会生成警告。但是,在这种情况下,您将无法使用JDK5.0中引入的任何新语言特性。
本文译自:https://docs.oracle.com/javase/tutorial/extra/generics/legacy.html