Java泛型—类型转换
如下代码编译无法通过:
class A{} class B extends A {} public static void funC(List<A> listA) { // ... } public static void funD(List<B> listB) { funC(listB); // ... }
Unresolved compilation problem: The method doPrint(List<A>) in the type test is not applicable for the arguments (List<B>)
而下面的代码就没问题:
public static void funC(A a) { // ... } public static void funD(B b) { funC(b); // ... }
在第二段代码中,类型B的实例向上转换成类型A的实例传入函数funC(A a),这是正常的隐式类型转换。而第一段代码则表明类型List<B>的实例无法转换成类型List<A>的实例。这就引出了Java泛型的类型转换问题。
Java是在JDK 5中引入的泛型(generics)新特性的。最主要的应用是在JDK 5中的新集合类框架中。可以解决之前的集合类框架在使用过程中出现的运行时刻类型错误,把问题暴露在编译中。但是为了保证与旧版本的兼容性,Java泛型的实现采用了类型擦除的机制,带来了一些缺陷。Java泛型的分析参考:http://www.infoq.com/cn/articles/cf-java-generics/。
Java泛型的实现:类型擦除
以上问题在C++中不会出现,这是两种语言的泛型实现机制不同造成的。
C++中的泛型类或方法,编译器会自动检查实例化的参数类型,然后根据类型检查参数类型的调用是否合理。如下代码能很好说明这个问题,模板在定义时并不对参数类型T的操作提前限制,等模板实例化时会根据T的具体类型检查操作是否合理。
template<class T> class Manipulator { T obj; public: Manipulator(T x) {obj = x; } void manipulate() {obj.f(); } }; class HasF { void f() { // ... } }; int main() { HasF hf; Manipulator<HasF> manipulator(hf); manipulator.manipulate(); }
Java采用的是所谓的“擦除”机制。在编译时不考虑传入的类型,把该类型的印迹全部擦除,视该类型为最基本的Object类型。但是Java会在编译时检查泛型接口传入的类型参数的实例是否存在隐含的转换情况,为了安全,泛型禁止此类转换。这就是开始我们遇到的问题,编译器检测到需要把List<B>实例转换成List<A>实例,这是被禁止的。明明可以实现的类型转换却被禁止,原因是编译时把类型参数擦除,当做Object,在运行时还要把Object类型的实例转换成类型参数的实例,两次转换存在运行时错误的风险。
为了解决泛型中隐含的转换问题,Java泛型加入了类型参数的上下边界机制。<? extends A>表示该类型参数可以是A(上边界)或者A的子类类型。编译时擦除到类型A,即用A类型代替类型参数。这种方法可以解决开始遇到的问题,编译器知道类型参数的范围,如果传入的实例类型B是在这个范围内的话允许转换,这时只要一次类型转换就可以了,运行时会把对象当做A的实例看待。
引入上边界使Java泛型具有了像C++泛型那样在模板中调用类型参数的方法的能力。如下Java代码,实现了上面C++代码同样的功能:
class Manipulator<T extends hasF> { T hf; public: Manipulator(T t) {hf = t; } void manipulate() {hf.f(); } };