在探讨类型擦除之前,我们还是先了解一下,泛型的概念。
泛型是为了参数化类型,定义方法时传入形参,而调用方法时使用形参,参数化类型就是由确定的类型参数化,改为不确定的类型,而在调用时使用具体类型的参数,从而实现解耦。这种参数类型可以用于类、接口、方法中,分别被称为泛型类、泛型接口和泛型方法。
这里的栗子是别人写的,个人觉得挺形象的,粘贴一下
试想你需要一个简单的容器类,或者说句柄类,比如要存放一个苹果的篮子,那你可以这样简单的实现:
class Fruit{}
class Apple extends Fruit{}
class Bucket{
private Apple apple;
public void set(Apple apple){
this.apple = apple;
}
public Apple get(){
return this.apple;
}
}
这样一个简单的篮子就实现了,但问题是它只能存放苹果,之后又出现了另外的一大堆水果类,那你就不得不为这些水果类分别实现容器:
class Fruit{}
class Apple extends Fruit{}
class Banana extends Fruit{}
class Orange extends Fruit{}
class BucketApple{
private Apple apple;
public void set(Apple apple){
this.apple = apple;
}
public Apple get(){
return this.apple;
}
}
class BucketBanana{
private Banana banana;
public void set(Banana banana){
this.banana = banana;
}
public Banana get(){
return this.banana;
}
}
class BucketOrange{
private Orange orange;
public void set(Orange orange){
this.orange = orange;
}
public Orange get(){
return this.orange;
}
}
然后你发现你其实在做大量的重复劳动。所以你幻想你的语言编译器要是支持某一种功能,能够帮你自动生成这些代码就好了。
不过在祈求让编译器帮你生成这些代码之前,你突然想到了Object能够引用任何类型的对象,所以你只要写一个Object类型的Bucket就可以存放任何类型了。
class Bucket{
private Object object;
public void set(Object object){
this.object = object;
}
public Object get(){
return this.object;
}
}
但是问题是这种容器的类型丢失了,你不得不在输出的地方加入类型转换:
Bucket appleBucket = new Bucket();
bucket.set(new Apple());
Apple apple = (Apple)bucket.get();
而且你无法保证被放入容器的就是Apple,因为Object可以指向任何引用类型。
这个时候你可能又要祈求编译器来帮你完成这些类型检查了。
说到这里,你应该明白了泛型要保证两件事,
第一:我只需要定义一次类,就可以被“任何”类型使用,而不是对每一种类型定义一个类。
第二:保存对象类型,而不用每次都强转
通过以上的栗子,我们对泛型有一个大致的了解,但是泛型是怎么来实现以上特性的呢?这里就引入这篇文章的标题的内容,‘类型擦除’
我这里自己写了一个泛型java类,很简单
public class TestGeneric<T> {
public T instance;
public TestGeneric(T hello) {
instance=hello;
System.out.println(hello.getClass().getCanonicalName());
}
}
编译以后生成的class文件
public class TestGeneric<T> {
public T instance;
public TestGeneric(T hello) {
this.instance = hello;
System.out.println(hello.getClass().getCanonicalName());
}
}
对,你没看错就是一样的。但是我们把鼠标放到T上,会看到T扩展自Object,所以泛型根本上还是通过Object来实现的类型扩展。
我们接着看以下泛型在调用时的情况
public class TestGenericCore {
public void show(){
Apple apple=new Apple();
Banana banana=new Banana();
TestGeneric<Apple> appleTestGeneric=new TestGeneric<>(apple);
//不用强转就可以直接调用指定类的成员方法,这就是泛型的好处
appleTestGeneric.instance.showApple();
TestGeneric<Banana> bananaTestGeneric=new TestGeneric<>(banana);
bananaTestGeneric.instance.showBanana();
}
}
public class Apple {
public void showApple(){}
}
public class Banana {
public void showBanana(){}
}
这里我们分别传入两个类,并且为了区分,成员方法分别是showApple和showBanana。
我们接着看一下以上TestGenericCore的class文件是怎么样的。
public class TestGenericCore {
public TestGenericCore() {
}
public void show() {
Apple apple = new Apple();
Banana banana = new Banana();
TestGeneric<Apple> appleTestGeneric = new TestGeneric(apple);
((Apple)appleTestGeneric.instance).showApple();
TestGeneric<Banana> bananaTestGeneric = new TestGeneric(banana);
((Banana)bananaTestGeneric.instance).showBanana();
}
}
强转出现……所以泛型最后在编译时也会替我们把隐去的强转加上,可以说是非常贴心了。
至于类型擦除,就是在编译期间,根据我们声明的泛型类型进行静态类型检查,然后再将类型擦除,改为Object。所谓的类型检查就是在对象出现的地方,例如(赋值语句,函数调用的实参和形参检查、return返回的类型必须兼容)判断类型是否符合约束。
因为类型会被擦除,所以以下方法会报错,因为在编译看来,形参是一样的。
好了,本篇只是用来记录一下自己搞懂类型擦除的过程,希望对大家有所帮助。