Java的泛型实现采用了擦除(erasure)机制,这给获取泛型的类型信息带来了一点麻烦。比如下面的代码(摘自Thinking in Java):
import java.util.*;
class Frob {}
class Fnorkle {}
class Quark<Q> {}
class Particle<POSITION,MOMENTUM> {}
public class LostInformation {
public static void main(String[] args) {
List<Frob> list = new ArrayList<Frob>();
Map<Frob,Fnorkle> map = new HashMap<Frob,Fnorkle>();
Quark<Fnorkle> quark = new Quark<Fnorkle>();
Particle<Long,Double> p = new Particle<Long,Double>();
System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
System.out.println(Arrays.toString(map.getClass().getTypeParameters()));
System.out.println(Arrays.toString(quark.getClass().getTypeParameters()));
System.out.println(Arrays.toString(p.getClass().getTypeParameters()));
}
}
它的输出是:
[E]
[K, V]
[Q]
[POSITION, MOMENTUM]
代码里面使用了Class.getTypeParameters( )方法,这个方法在JDK的文档里面的说明为:returns an array of TypeVariable objects that represent the type variables declared by the generic declaration...。也就是说她能够返回泛型声明中的变量的类型。
可能你会认为通过这个方法就能够拿到使用泛型时候的具体变量类型。但是事实并非如此,你能拿到的仅仅是在泛型声明中用作占位符的标识符号。
所以,事实就是,在使用泛型的代码里面你是没有办法拿到关于泛型参数的任何类型信息的,也就是你无法动态地知道泛型参数的具体类型。
这就是Java实现泛型时候的擦除机制。当你使用泛型的时候,Java会擦去所有的与特定类型有关的信息。你知道的仅仅是你正在使用一个对象而已。在Java看来,List<Integer>和List<String>是同一个类型,因为他们的特定类型信息都被擦除到原始类型了,也就是List,具体的类型信息都被擦掉了。
由于Java的擦除,使得在C++里面看起来很合理的代码,到了Java就不行了,比如有这样的C++ template代码:
#include <iostream>
using namespace std;
template<class T> class Manipulator {
T obj;
public:Manipulator(T x) { obj = x; }
void manipulate() { obj.f(); }
};
class HasF {
public:
void f() { cout << "HasF::f()" << endl; }
};
int main() {
HasF hf;
Manipulator<HasF> manipulator(hf);
manipulator.manipulate();
}
/* Output:
HasF::f()
上面的代码中,Manipulator类持有一个T类型的泛型参数。注意manipulate方法,在这个方法里面直接调用了f()函数。C++在初始化template的时候进行检查,编译器会检查你传入的HasF类是不是包含有f()函数。这种做法也是保证类型安全的。
但是在Java里面,如果将上面的代码直接翻译过来:
//: generics/Manipulation.java
// {CompileTimeError} (Won’t compile)
class Manipulator<T> {
private T obj;
public Manipulator(T x) { obj = x; }
// Error: cannot find symbol: method f():
public void manipulate() { obj.f(); }
}
public class Manipulation {
public static void main(String[] args) {
HasF hf = new HasF();
Manipulator<HasF> manipulator =
new Manipulator<HasF>(hf);
manipulator.manipulate();
}
} ///:~
这个时候Java编译器是会报错的,原因就是Java的擦除。在编译和运行时是无法知道泛型代码中的具体类型信息的。所以为了能够使用f()方法,在Java中我们就需要为泛型代码加绑定(bound)。如下:
class Manipulator2<T extends HasF> {
private T obj;
public Manipulator2(T x) { obj = x; }
public void manipulate() { obj.f(); }
} ///:~
<T extends HasF>表示T必须是HasF类或者它的子类。如果这一条满足了,那么调用f()就是安全的。
这里可以理解为,T被擦除到了HasF这个类型。你会发现在Java中这样使用泛型其实没什么用处,你可以轻而易举地去掉泛型而得到一段与使用了泛型一模一样的代码:
class Manipulator3 {
private HasF obj;
public Manipulator3(HasF x) { obj = x; }
public void manipulate() { obj.f(); }
} ///:~
所以,在Java中,合适使用泛型的情况是当你需要你的类或者方法支持多个类型的时候。但是也不能说<T extends HasF>这种形式就没有好处,比如当你的方法返回T的时候,你就能得到具体类型,而不用作强制类型转换了。
因为泛型并不是自Java诞生之日起就有的,因此Java之所以有这样的擦除机制,不是因为Sun的大牛们不知道问题的存在,而是为了兼容以前的Java版本。所以,可以说擦除并不是Java语言的特性。为了使得JDK1.5以前的代码能够使用带泛型特征的类库,或者1.5之后的代码能够使用1.5版本以前的类库,Java选择了擦除。
备注:
不能使用Java泛型类型的情况:
[list=1]
需要使用具体运行时类型信息的情况,如类型转换,instranceof运算符,new运算符等。当你定义了一个类,
class Foo<T> {
T var;
}
并且这样使用时,
Foo<Cat> f = new Foo<Cat>();
不要以为你的变量var已经是Cat类型了,一定要记住,它是Object类型的,因为Java的擦除。
因为擦除和版本兼容,泛型并不是被强制的。如下面代码:
class GenericBase<T> {
private T element;
public void set(T arg) { arg = element; }
public T get() { return element; }
}
class Derived1<T> extends GenericBase<T> {}
class Derived2 extends GenericBase {} // No warning
// class Derived3 extends GenericBase<?> {}
// Strange error:
// unexpected type found : ?
// required: class or interface without bounds
public class ErasureAndInheritance {
@SuppressWarnings("unchecked")
public static void main(String[] args) {
Derived2 d2 = new Derived2();
Object obj = d2.get();
d2.set(obj); // Warning here!
}
} ///:~
Derivered2在继承GenericBase的时候没有使用泛型语法,但是编译器并没有警告提示,只有当调用d2.set()的时候编译器才提示了警告信息。从这个例子也可以看出,泛型的具体类型信息确实是被擦除掉了。在这里,泛型类型T被擦成了Object。
[/list]