这篇文章先对比了一下C++的泛型代码,能让你更清楚的感受到擦除的效果(请放心,只是简单的c++代码,不了解c++的同学也能看的懂)。
先看下C++的泛型:
#include
using namespace std;
template
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
manipulator(hf); manipulator.manipulate();
}
/*Output: HasF::f() */
Manipulator类存储了一个类型T的对象,有意思的地方是manipulate()方法,这个方法中obj调用了方法f()。在C++中当你实例化这个模板时,C++编译器将进行检查,因此在Manipulator
接下来看一下Java中的代码:
publicclass HasF {
public void f() {System.out.println("HasF.f()");}}
}
publicclass Manipulator
{ private T obj;
public Manipulator(T obj) {
this.obj = obj;
}
public void manipulate() {
// obj.f();//此处报错
}
}
public class Manipulation {
public static void main(String[] args) {
HasF hasF = new HasF();
Manipulator
manipulator = new Manipulation(hasF); manipulator.manipulate();
}
}
可以看出Java和C++泛型代码很明显区别就是C++中的泛型有实际类型信息,而Java中的实际类型信息不管是编译时期还是在运行时期都被擦除了,这就是擦除的效果。由于有了擦除,Java编译器无法将obj调用f()这一需求映射到HasF拥有f()这一事实上。(事实上擦除是将泛型类型信息擦除到了它的第一个边界,默认不设置的边界是Object,你可以调用Object的方法,可以这样设置边界——
通过上面的对比应该能明显的感受到Java擦除的存在了吧?在Java中,当你使用泛型时,任何具体的类型信息都会被擦除,你唯一知道的就是你在使用一个对象。
那这样问题就来了——实际的泛型类型信息被擦除了,Java是怎么保障类型信息的正确性呢?看下一段Java代码:
public class FilledListMaker
{ List
create(T t, int n) { List
result = new ArrayList (); for (int i = 0; i < n; i++) {publicresult.add(t);}
}
public static void main(String[] args) {
FilledListMaker
stringMaker = new FilledListMaker (); List
list = stringMaker.create("hello", 4); System.out.println(list);
//System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
}
}
/*[hello,hello,hello,hello]/*
/* [E] /*
在这段代码中看起来好像是拥有了String参数类型信息,但实际并非如此。最后一行代码的意思是输出一个TypeVariable对象数组,数组表示泛型所声明的类型信息。但是,正如你看见的,输出的只是用作参数占位符的标识符,并非有用的信息。
在这上面这段代码中编译器无法知道有关create()中T的任何信息,但是它仍旧可以在编译期确保你放置到result中的对象具有T类型,使其适合ArrayList
擦除在方法体中移除了类型信息,所以在运行时的问题就是边界,即对象进入和离开方法的地点。这些正是编译器在编译期执行类型检查并插入转型代码的地点。(这段话中的边界和上方设置边界不是一个概念,别搞混了。)再看看下面的两段代码:
public class SimpleHolder {
private Object obj;
public Object getObj() { return obj; }
public void setObj(Object obj) { this.obj = obj; }
public static void main(String[] args) {
SimpleHolder simpleHolder = new SimpleHolder();
simpleHolder.setObj("Item");
String s = (String) simpleHolder.getObj();
}
}
public class GenericHolder
{ private T obj;
public T getObj() { return obj; }
public void setObj(T obj) { this.obj = obj; }
public static void main(String[] args) {
GenericHolder genericHolder = new GenericHolder();
genericHolder.setObj("Item");
String s = genericHolder.getObj();
}
}
如果用Java -c SimpleHolder反编译这个类,就可以得到下面的内容(直接上图了实在是懒得写了):
可以看出set()和get()方法直接存储和产生值,而转型是在调用get()的时候接受检查的。接下来看一下GenericHolder:
可以看出GenericHolder产生的字节码操作和SimpleHolder产生的字节码操作是相同的。对于进入set()的类型进行检查是不需要的,因为这将由编译器执行。而对从get()返回的值进行转型仍旧是需要的,但这与你自己必须执行的操作是一样的——此处它将由编译器自动插入。
那么问题又来了——为什么要有擦除?
擦除的核心动机是它使得泛化的客户端可以用非泛化的类库来使用,反之亦然,这经常被称为“迁移兼容性”。(因为泛型的概念是JavaSE5之后提出的,所以为了兼容以前的客户端和类库提出了擦除的概念,擦除也使得非泛化代码向着泛型的迁移成为了可能。)
擦除存在的主要理由是从非泛化代码到泛化代码的转变过程,以及在不破坏现有类库的情况下,将泛型融入Java语言中。擦除使得现有的非泛型客户端代码能够在不改变的情况下继续使用,直至客户端准备好用泛型重写这些代码。擦除的代价也是比较明显的,实际类型的信息丢失,无法使用转型、instanceof操作、和new表达式。当你在编写泛型代码时,必须要提醒自己你只是看起来好像拥有了有关参数类型信息而已,实际上这个泛型类型的实际类型信息将被擦除到第一个边界,你唯一知道的就是你只是在操作一个对象。
那么总结一下,擦除是擦掉了泛型类型的实际类型信息(擦除到了第一个边界),而编译器保障了类型的安全性,擦除的存在是为了实现迁移的兼容性。
有需要改进的地方,望指出,谢谢。