到目前为止,我们所有的示例都假设了一个理想化的世界,每个人都在使用支持泛型的Java编程语言的最新版本。
实际上并非如此。数百万行代码已用该语言的早期版本编写,而且它们不会在一夜之间全部转化。。
稍后,在 将旧版代码转换为使用泛型部分中,我们将解决将旧代码转换为使用泛型的问题。在本节中,我们将关注一个简单的问题:旧代码和泛型代码如何相互操作?这个问题分为两个部分:在泛型代码中使用旧代码,以及在旧代码中使用泛型代码。
在泛型代码中使用旧版代码
在仍然享受自己代码中泛型的好处的同时,如何使用旧代码?
例如,假设您要使用包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的集合。当然,泛型是为此而量身定制的:
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<Part>
的地方,传递一个Collection<Object>
是不安全的。准确地说,类型Collection
表示某种未知类型的集合,就像Collection<?>
。
但是,等等,那也不对!考虑对getParts()
的调用,该调用返回Collection
。然后将其赋值给k
,一个Collection<Part>
。如果调用的结果为Collection<?>
,则赋值将为错误。
实际上,该赋值是合法的,但是会生成未检查警告。需要警告,因为事实是编译器无法保证其正确性。我们无法检查旧代码,以确保getAssembly()
确实返回的集合是Part
的集合。代码中使用的类型是Collection
,并且可以合法地将各种对象插入此类集合中。
所以,这不应该是一个错误吗?从理论上讲,是的。但是实际上,如果泛型代码要调用旧代码,则必须允许这样做。在这种情况下,由程序员(您自己)决定,这是安全的,因为即使类型签名没有显示,确保getAssembly()
仍会返回Part
s 的集合,这是安全的。
因此,原始类型非常类似于通配符类型,但是没有严格地对它们进行类型检查。这是一个经过深思熟虑的设计决策,以允许泛型与预先存在的旧代码互操作。
从泛型代码中调用遗留代码本质上是危险的。一旦将泛型代码与非泛型遗留代码混合,泛型类型系统通常提供的所有安全保证都将失效。但是,与完全不使用泛型的情况相比,您的状况仍然更好。至少您知道自己的代码是一致的。
目前,存在更多的非泛型代码,然后是泛型代码,并且不可避免地会有必须混合使用的情况。
如果发现必须混合使用旧代码和泛型代码,请密切注意未检查警告。仔细考虑如何才能证明引起警告的代码的安全性。
如果您仍然犯了一个错误,并且引起警告的代码确实不是安全的,会发生什么?让我们来看看这种情况。在此过程中,我们将深入了解编译器的工作原理。
删除和翻译
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虚拟机的类型安全性和完整性也永远不会受到威胁。
基本上,擦除会删除(或擦除)所有泛型类型信息。排除了尖括号之间的所有类型信息,例如,将类似的参数化类型List<String>
转换为List
。类型变量的所有其余用法都由类型变量的上限(通常为Object
)代替。而且,只要结果代码的类型不正确,就会插入对相应类型的强制类型转换,如loophole
的最后一行。
擦除的全部细节不在本教程的讨论范围之内,但是我们刚刚给出的简单描述与事实并非遥不可及。最好对此有所了解,尤其是如果您想做更复杂的事情,例如将现有的API转换为使用泛型(请参阅 将旧版代码转换为使用泛型部分),或者只是想了解事物为何如此。
在旧版代码中使用泛型代码
现在让我们考虑相反的情况。想象一下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
确实是Part
的集合。
或者,您可以使用源1.4标志编译客户端代码,以确保不生成任何警告。但是,在这种情况下,您将无法使用JDK 5.0中引入的任何新语言功能。