引言
上一篇文章我们简单地了解了泛型的定义和使用,并且知道,在使用了泛型之后, 代码在编译期间就被确保是类型安全的,不会出现类转换异常。泛型通过什么手段来达到这个目的呢?我们这篇文章就来探讨这个问题。
不过这里不会首先就去讲解擦除的原理,而是通过对比反编译前后的泛型类文件来开始认识泛型擦除。
反编译后的结果
泛型类
我们还是以上一篇文章中的GenericHolder1为例:
public class GenericHolder1<T> {private T t;public GenericHolder1(T t) {this.t = t;}public T getT() {return t;}public void setT(T t) {this.t = t;}public static void main(String[] args) {GenericHolder1<String> genericHolder1 = new GenericHolder1<>("we");String t = genericHolder1.getT();}}
我们使用jad来反编译GenericHoler1生成的class文件,我们会看到下面的结果:
public class GenericHolder1{public GenericHolder1(Object t){this.t = t;}public Object getT(){return t;}public void setT(Object t){this.t = t;}public static void main(String args[]){GenericHolder1 genericHolder1 = new GenericHolder1("we");String t = (String)genericHolder1.getT();}private Object t;}
我们可以发现,反编译后的文件没有一点泛型的影子,GenericHolder1这个类的声明没有其他的东西了,只是一个Public Class GenericHolder1,这就是一个简单的类声明。在每个出现T的位置,都用Object进行了替换,main方法里面对getT方法的调用,编译器自动为我们做了强制类型转换(因为在创建GenericHolder1的实例时,我们已经指定了泛型的精确类型是String)。也就是说,编译器用Object替换了T,但是当真正指定了T的精确类型之后,还是需要做强制类型转换的,只是编译器为我们自动做了,这也就是为什么不需要我们再去进行类型转换而直接可以得到String的原因。
这就是泛型的擦除,在编译期间,泛型信息被完全擦除,真正运行的是一个没有泛型信息的类。
再来看另外一个例子:
public class GenericHolder2<T extends SimpleObject> {private T t;public GenericHolder2(T t) {this.t = t;}public T getT() {return t;}public void setT(T t) {this.t = t;}public static void main(String[] args) {GenericHolder2<SimpleObject> genericHolder2 = new GenericHolder2<>(new SimpleObject());SimpleObject s = genericHolder2.getT();}}
与GenericHolder1不同的是,我们的泛型参数T这次继承了SimpleObject,意思就是T可以是任何SimpleObject的子类。反编译后的文件如下:
public class GenericHolder2{public GenericHolder2(SimpleObject t){this.t = t;}public SimpleObject getT(){return t;}public void setT(SimpleObject t){this.t = t;}public static void main(String args[]){GenericHolder2 genericHolder2 = new GenericHolder2(new SimpleObject());SimpleObject s = genericHolder2.getT();}private SimpleObject t;}
替换掉T的不再是Object,而是SimpleObject,与Object不同的是,编译器不再需要为我们做强制类型转换了,因为每个用到T的位置,不管是getT的返回值,还是setT的参数,都是精确的SimpleObject的类型。
通过这两个例子,我们应该了解了泛型擦除的原理,就是在编译期间将泛型参数替换为真实的边界,然后在每个用到泛型的位置通过加上强制类型转换(Object)或者不加(例如SimpleObject)来实现类型的安全转换。泛型类能够做到这点很重要的原因是,在初始化泛型类或者泛型接口时,我们需要指定精确的泛型类型,编译器通过我们指定的精确类型,来完成擦除。
泛型方法
我们再来看上一篇文章中泛型方法的例子:
public class GenericMethods {public <T> T getObject(T t){return t;}public static void main(String[] args) {GenericMethods genericMethods = new GenericMethods();Integer object = genericMethods.getObject(1);String qwe = genericMethods.getObject("qwe");SimpleObject object1 = genericMethods.getObject(new SimpleObject());}}
泛型方法与泛型类、泛型接口不同的是,没有初始化的步骤,只是直接的参数传递,那么泛型方法是怎样保证类型安全的呢?还是看反编译后的代码:
public class GenericMethods{public GenericMethods(){}public Object getObject(Object t){return t;}public static void main(String args[]){GenericMethods genericMethods = new GenericMethods();Integer object = (Integer)genericMethods.getObject(Integer.valueOf(1));String qwe = (String)genericMethods.getObject("qwe");SimpleObject object1 = (SimpleObject)genericMethods.getObject(new SimpleObject());}}
T还是被替换成了Object,然后编译器同样为方法返回值加上了强制类型转换,那么它怎么知道需要转换成什么类型呢?因为我们在传递参数的时候已经说明了T的类型,例如当传入1时说明类型是Integer,编译器就知道返回值是Integer,当传入SimpleObject时编译器就知道要返回SimpleObject。
再来看下面这个方法:
public class GenericMethods {public <T extends SimpleObject,E> E getObject1(T t,E e){System.out.println(e.getClass().getName());return e;}public static void main(String[] args) {GenericMethods genericMethods = new GenericMethods();Integer integer = genericMethods.getObject1(new SimpleObject(), 1);String string = genericMethods.getObject1(new SimpleObject(), "a string");}}
getObject1方法有两个泛型参数,T指定必须是SimpleObject的子类,E没有指定边界,所以我们猜想,T会被替换为SimpleObject,E会被替换为Object,看反编译后的代码:
public class GenericMethods{public GenericMethods(){}public Object getObject1(SimpleObject t, Object e){System.out.println(e.getClass().getName());return e;}public static void main(String args[]){GenericMethods genericMethods = new GenericMethods();Integer integer = (Integer)genericMethods.getObject1(new SimpleObject(), Integer.valueOf(1));String string = (String)genericMethods.getObject1(new SimpleObject(), "a string");}}
结果和我们的猜想一样。由于返回值类型被替换为Object,所以在main方法中的调用,编译器自动为我们加上了强制类型转换来保证类型安全。
通过上面的四个例子,我们知道了擦除是怎样实现的:在泛型类或者泛型接口中,编译器通过对泛型类型参数的声明来选择替换成哪种准确的类型,例如将没有边界的T替换为Object,将T extends SimpleObject替换为SImpleObject。由于我们在初始化的时候需要为泛型参数指定精确的类型,所以编译器会知道我们想要什么类型,就能够通过自动加上类型转换(当被擦除为Object的时候)或者不加(当被替换为类似SimpleObject的时候)来保证类型的安全转换。
在泛型方法中,编译器同样通过泛型参数的声明来进行擦除,虽然没有初始化过程,但是编译器能够通过我们传递的参数判断出我们想要的类型,进而保证类型安全。
由于泛型在编译期间被擦除了,所以在运行期间,每个泛型实例的类型总是相同的,而不管它们指定了什么样的精确类型。看下面的例子:
public static void main(String[] args) {GenericHolder1<String> a = new GenericHolder1<>("we");GenericHolder1<Integer> b = new GenericHolder1<>(1);System.out.println(a.getClass() == b.getClass());System.out.println(a.getClass().getName());}
输出:
person.andy.concurrency.generic.GenericHolder1true
这就是使用擦除来实现泛型的结果。a和b虽然在声明时分别指定了String和Integer作为类型参数的精确类型,但是GenericHolder1这个类在编译期间就被擦除为普通的类型了,所以a和b都只是被擦除后的普通类型而已。
类似的,java中的集合也会是这样的情况:
public static void main(String[] args) {List<String> strings = new ArrayList<>();List<Integer> integers = new ArrayList<>();System.out.println(strings.getClass()==integers.getClass());}
使用桥接方法来保护多态
当类型信息被擦除后,方法的继承就会出现问题,看下面的例子:
public class ObjectContainer<T> {private T contained;public ObjectContainer(T contained) {this.contained = contained;}public T getContained() {return contained;}public void setContained(T contained) {this.contained = contained;}}public class FruitContainer extends ObjectContainer<Fruit> {public FruitContainer(Fruit contained) {super(contained);}@Overridepublic void setContained(Fruit contained) {super.setContained(contained);}public static void main(String[] args) {FruitContainer fruitContainer = new FruitContainer(new Apple());fruitContainer.setContained(new Apple());}}
FruitContainer继承了ObjectContainer并且指定了精确类型Fruit,所以setContained()方法的参数变成了Fruit,而ObjectContainer的setContained()方法在被擦除后应该是Object,在父类和子类中的方法参数类型并不相同,也就是FruitContainer的setContained()方法并没有override ObjectContainer的setContained()方法,当遇到这种情况,编译器会自动为我们添加一个桥方法,看FruitContainer反编译后的文件:
public class FruitContainer extends ObjectContainer{public FruitContainer(Fruit contained){super(contained);}public void setContained(Fruit contained){super.setContained(contained);}public static void main(String args[]){FruitContainer fruitContainer = new FruitContainer(new Apple());fruitContainer.setContained(new Apple());}public volatile void setContained(Object obj){setContained((Fruit)obj);}}
可以看到反编译后的文件比源文件多了一个setContained(Object obj)方法,这个方法直接调用了原有的setContained()这个方法,这就是桥方法,编译器通过自动添加这个方法,保护了继承关系中的多态。
边界
在前面的GenericHolder1中,我们简单的使用了T指定了泛型参数,通过编译后的代码我们知道,T被替换成了Object,在没有给定任何边界的情况下,Object会作为泛型参数的默认边界。
但是有时我们想调用特定类或者接口的方法,这时我们可以通过定义泛型的边界来实现。GenericHolder2就是一个使用了自定义边界的例子,我们通过extends方法指定泛型参数继承了SimpleObject,此时T的边界就是SimpleObject。
从编译后的文件我们也能看到。当有了SimpleObject作为边界之后,我们就可以执行它的方法了,看下面的例子:
public interface HasColor {java.awt.Color getColor();}
public class WithColor<T extends HasColor> {private T item;public WithColor(T item) {this.item = item;}public T getItem() {return item;}public java.awt.Color getColor(){return item.getColor();}}
泛型类WithColor的类型变量T被指定需要实现HasColor接口,所以边界就是HasColor,这样就可以在getColor里面调用item的getColor方法了。
根据之前的经验,这个类反编译之后T会被HasColor替代,所以对其调用getColor肯定是可以的:
public class WithColor{public WithColor(HasColor item){this.item = item;}public HasColor getItem(){return item;}public Color getColor(){return item.getColor();}private HasColor item;}
继续我们的例子,我们添加一个类Coord:
public class Coord {public int x,y,z;public int getCoords(){return x + y + z;};}
然后添加泛型类,该泛型类的类型参数需要继承Coord并实现HasColor:
public class WithColorCoord<T extends Coord & HasColor> {T item;public WithColorCoord(T item) {this.item = item;}public T getItem() {return item;}java.awt.Color getColor(){return item.getColor();}int getX(){return item.x;}int getY(){return item.y;}int getZ(){return item.z;}int getCoord(){return item.getCoords();}}
T类型的item能够调用Coord和HasColor两者的方法,我们看一下反编译后的代码:
public class WithColorCoord{public WithColorCoord(Coord item){this.item = item;}public Coord getItem(){return item;}Color getColor(){return ((HasColor)item).getColor();}int getX(){return item.x;}int getY(){return item.y;}int getZ(){return item.z;}int getCoord(){return item.getCoords();}Coord item;}
编译器用Coord类替换了T,然后在每个调用Coord的方法上,就能直接使用了,而在调用HasColor的方法时,进行了强制类型转换,这种转换也是没有问题的。
继续增加一个接口:
interface Weight {int weight();}
修改泛型类:
public class Solid<T extends Coord & HasColor & Weight> {T item;public Solid(T item) {this.item = item;}public T getItem() {return item;}java.awt.Color color(){return item.getColor();}int getX(){return item.x;}int getY(){return item.y;}int getZ(){return item.z;}int weight(){return item.weight();}}
增加了weight接口之后,我们也可以在T上面调用weight的方法了,看编译后的代码:
public class Solid{public Solid(Coord item){this.item = item;}public Coord getItem(){return item;}Color color(){return ((HasColor)item).getColor();}int getX(){return item.x;}int getY(){return item.y;}int getZ(){return item.z;}int weight(){return ((Weight)item).weight();}Coord item;}
item还是被Coord替代,在使用weight和hasColor的部分,都是通过强制类型转换来做的。
也就是说,对于泛型,在没有给定边界的情况下,默认边界就是Object,在给定边界的情况下,边界是extends关键字后面的第一个类型T,注意当extends后面有多个类型例如
还有一点需要注意,在声明泛型类、泛型接口和泛型方法的类型变量时,都只能使用extends来指定边界,而不能使用super,这个要跟后面将要讲到的协变和逆变分清楚。
小结
这篇文章从泛型类和泛型方法反编译后的文件出发,分析了java泛型擦除的实现方式,我们应该知道编译器在泛型擦除的过程中起了很大的作用,它根据我们的泛型定义生成了擦除后的代码,为泛型找到了合适的边界。使用擦除来实现泛型同样导致了很多问题,例如不能使用instanceOf关键字,这些都可以通过其他方式来解决。
下一篇文章,我们会介绍泛型的另外一个问题—协变和逆变。
