引言

上一篇文章我们简单地了解了泛型的定义和使用,并且知道,在使用了泛型之后, 代码在编译期间就被确保是类型安全的,不会出现类转换异常。泛型通过什么手段来达到这个目的呢?我们这篇文章就来探讨这个问题。
不过这里不会首先就去讲解擦除的原理,而是通过对比反编译前后的泛型类文件来开始认识泛型擦除。

反编译后的结果

泛型类

我们还是以上一篇文章中的GenericHolder1为例:

  1. public class GenericHolder1<T> {
  2. private T t;
  3. public GenericHolder1(T t) {
  4. this.t = t;
  5. }
  6. public T getT() {
  7. return t;
  8. }
  9. public void setT(T t) {
  10. this.t = t;
  11. }
  12. public static void main(String[] args) {
  13. GenericHolder1<String> genericHolder1 = new GenericHolder1<>("we");
  14. String t = genericHolder1.getT();
  15. }
  16. }

我们使用jad来反编译GenericHoler1生成的class文件,我们会看到下面的结果:

  1. public class GenericHolder1
  2. {
  3. public GenericHolder1(Object t)
  4. {
  5. this.t = t;
  6. }
  7. public Object getT()
  8. {
  9. return t;
  10. }
  11. public void setT(Object t)
  12. {
  13. this.t = t;
  14. }
  15. public static void main(String args[])
  16. {
  17. GenericHolder1 genericHolder1 = new GenericHolder1("we");
  18. String t = (String)genericHolder1.getT();
  19. }
  20. private Object t;
  21. }

我们可以发现,反编译后的文件没有一点泛型的影子,GenericHolder1这个类的声明没有其他的东西了,只是一个Public Class GenericHolder1,这就是一个简单的类声明。在每个出现T的位置,都用Object进行了替换,main方法里面对getT方法的调用,编译器自动为我们做了强制类型转换(因为在创建GenericHolder1的实例时,我们已经指定了泛型的精确类型是String)。也就是说,编译器用Object替换了T,但是当真正指定了T的精确类型之后,还是需要做强制类型转换的,只是编译器为我们自动做了,这也就是为什么不需要我们再去进行类型转换而直接可以得到String的原因。
这就是泛型的擦除,在编译期间,泛型信息被完全擦除,真正运行的是一个没有泛型信息的类。
再来看另外一个例子:

  1. public class GenericHolder2<T extends SimpleObject> {
  2. private T t;
  3. public GenericHolder2(T t) {
  4. this.t = t;
  5. }
  6. public T getT() {
  7. return t;
  8. }
  9. public void setT(T t) {
  10. this.t = t;
  11. }
  12. public static void main(String[] args) {
  13. GenericHolder2<SimpleObject> genericHolder2 = new GenericHolder2<>(new SimpleObject());
  14. SimpleObject s = genericHolder2.getT();
  15. }
  16. }

与GenericHolder1不同的是,我们的泛型参数T这次继承了SimpleObject,意思就是T可以是任何SimpleObject的子类。反编译后的文件如下:

  1. public class GenericHolder2
  2. {
  3. public GenericHolder2(SimpleObject t)
  4. {
  5. this.t = t;
  6. }
  7. public SimpleObject getT()
  8. {
  9. return t;
  10. }
  11. public void setT(SimpleObject t)
  12. {
  13. this.t = t;
  14. }
  15. public static void main(String args[])
  16. {
  17. GenericHolder2 genericHolder2 = new GenericHolder2(new SimpleObject());
  18. SimpleObject s = genericHolder2.getT();
  19. }
  20. private SimpleObject t;
  21. }

替换掉T的不再是Object,而是SimpleObject,与Object不同的是,编译器不再需要为我们做强制类型转换了,因为每个用到T的位置,不管是getT的返回值,还是setT的参数,都是精确的SimpleObject的类型。
通过这两个例子,我们应该了解了泛型擦除的原理,就是在编译期间将泛型参数替换为真实的边界,然后在每个用到泛型的位置通过加上强制类型转换(Object)或者不加(例如SimpleObject)来实现类型的安全转换。泛型类能够做到这点很重要的原因是,在初始化泛型类或者泛型接口时,我们需要指定精确的泛型类型,编译器通过我们指定的精确类型,来完成擦除。

泛型方法

我们再来看上一篇文章中泛型方法的例子:

  1. public class GenericMethods {
  2. public <T> T getObject(T t){
  3. return t;
  4. }
  5. public static void main(String[] args) {
  6. GenericMethods genericMethods = new GenericMethods();
  7. Integer object = genericMethods.getObject(1);
  8. String qwe = genericMethods.getObject("qwe");
  9. SimpleObject object1 = genericMethods.getObject(new SimpleObject());
  10. }
  11. }

泛型方法与泛型类、泛型接口不同的是,没有初始化的步骤,只是直接的参数传递,那么泛型方法是怎样保证类型安全的呢?还是看反编译后的代码:

  1. public class GenericMethods
  2. {
  3. public GenericMethods()
  4. {
  5. }
  6. public Object getObject(Object t)
  7. {
  8. return t;
  9. }
  10. public static void main(String args[])
  11. {
  12. GenericMethods genericMethods = new GenericMethods();
  13. Integer object = (Integer)genericMethods.getObject(Integer.valueOf(1));
  14. String qwe = (String)genericMethods.getObject("qwe");
  15. SimpleObject object1 = (SimpleObject)genericMethods.getObject(new SimpleObject());
  16. }
  17. }

T还是被替换成了Object,然后编译器同样为方法返回值加上了强制类型转换,那么它怎么知道需要转换成什么类型呢?因为我们在传递参数的时候已经说明了T的类型,例如当传入1时说明类型是Integer,编译器就知道返回值是Integer,当传入SimpleObject时编译器就知道要返回SimpleObject。
再来看下面这个方法:

  1. public class GenericMethods {
  2. public <T extends SimpleObject,E> E getObject1(T t,E e){
  3. System.out.println(e.getClass().getName());
  4. return e;
  5. }
  6. public static void main(String[] args) {
  7. GenericMethods genericMethods = new GenericMethods();
  8. Integer integer = genericMethods.getObject1(new SimpleObject(), 1);
  9. String string = genericMethods.getObject1(new SimpleObject(), "a string");
  10. }
  11. }

getObject1方法有两个泛型参数,T指定必须是SimpleObject的子类,E没有指定边界,所以我们猜想,T会被替换为SimpleObject,E会被替换为Object,看反编译后的代码:

  1. public class GenericMethods
  2. {
  3. public GenericMethods()
  4. {
  5. }
  6. public Object getObject1(SimpleObject t, Object e)
  7. {
  8. System.out.println(e.getClass().getName());
  9. return e;
  10. }
  11. public static void main(String args[])
  12. {
  13. GenericMethods genericMethods = new GenericMethods();
  14. Integer integer = (Integer)genericMethods.getObject1(new SimpleObject(), Integer.valueOf(1));
  15. String string = (String)genericMethods.getObject1(new SimpleObject(), "a string");
  16. }
  17. }

结果和我们的猜想一样。由于返回值类型被替换为Object,所以在main方法中的调用,编译器自动为我们加上了强制类型转换来保证类型安全。
通过上面的四个例子,我们知道了擦除是怎样实现的:在泛型类或者泛型接口中,编译器通过对泛型类型参数的声明来选择替换成哪种准确的类型,例如将没有边界的T替换为Object,将T extends SimpleObject替换为SImpleObject。由于我们在初始化的时候需要为泛型参数指定精确的类型,所以编译器会知道我们想要什么类型,就能够通过自动加上类型转换(当被擦除为Object的时候)或者不加(当被替换为类似SimpleObject的时候)来保证类型的安全转换。
在泛型方法中,编译器同样通过泛型参数的声明来进行擦除,虽然没有初始化过程,但是编译器能够通过我们传递的参数判断出我们想要的类型,进而保证类型安全。

由于泛型在编译期间被擦除了,所以在运行期间,每个泛型实例的类型总是相同的,而不管它们指定了什么样的精确类型。看下面的例子:

  1. public static void main(String[] args) {
  2. GenericHolder1<String> a = new GenericHolder1<>("we");
  3. GenericHolder1<Integer> b = new GenericHolder1<>(1);
  4. System.out.println(a.getClass() == b.getClass());
  5. System.out.println(a.getClass().getName());
  6. }

输出:

  1. person.andy.concurrency.generic.GenericHolder1
  2. true

这就是使用擦除来实现泛型的结果。a和b虽然在声明时分别指定了String和Integer作为类型参数的精确类型,但是GenericHolder1这个类在编译期间就被擦除为普通的类型了,所以a和b都只是被擦除后的普通类型而已。
类似的,java中的集合也会是这样的情况:

  1. public static void main(String[] args) {
  2. List<String> strings = new ArrayList<>();
  3. List<Integer> integers = new ArrayList<>();
  4. System.out.println(strings.getClass()==integers.getClass());
  5. }

输出结果也是true。

使用桥接方法来保护多态

当类型信息被擦除后,方法的继承就会出现问题,看下面的例子:

  1. public class ObjectContainer<T> {
  2. private T contained;
  3. public ObjectContainer(T contained) {
  4. this.contained = contained;
  5. }
  6. public T getContained() {
  7. return contained;
  8. }
  9. public void setContained(T contained) {
  10. this.contained = contained;
  11. }
  12. }
  13. public class FruitContainer extends ObjectContainer<Fruit> {
  14. public FruitContainer(Fruit contained) {
  15. super(contained);
  16. }
  17. @Override
  18. public void setContained(Fruit contained) {
  19. super.setContained(contained);
  20. }
  21. public static void main(String[] args) {
  22. FruitContainer fruitContainer = new FruitContainer(new Apple());
  23. fruitContainer.setContained(new Apple());
  24. }
  25. }

FruitContainer继承了ObjectContainer并且指定了精确类型Fruit,所以setContained()方法的参数变成了Fruit,而ObjectContainer的setContained()方法在被擦除后应该是Object,在父类和子类中的方法参数类型并不相同,也就是FruitContainer的setContained()方法并没有override ObjectContainer的setContained()方法,当遇到这种情况,编译器会自动为我们添加一个桥方法,看FruitContainer反编译后的文件:

  1. public class FruitContainer extends ObjectContainer
  2. {
  3. public FruitContainer(Fruit contained)
  4. {
  5. super(contained);
  6. }
  7. public void setContained(Fruit contained)
  8. {
  9. super.setContained(contained);
  10. }
  11. public static void main(String args[])
  12. {
  13. FruitContainer fruitContainer = new FruitContainer(new Apple());
  14. fruitContainer.setContained(new Apple());
  15. }
  16. public volatile void setContained(Object obj)
  17. {
  18. setContained((Fruit)obj);
  19. }
  20. }

可以看到反编译后的文件比源文件多了一个setContained(Object obj)方法,这个方法直接调用了原有的setContained()这个方法,这就是桥方法,编译器通过自动添加这个方法,保护了继承关系中的多态。

边界

在前面的GenericHolder1中,我们简单的使用了T指定了泛型参数,通过编译后的代码我们知道,T被替换成了Object,在没有给定任何边界的情况下,Object会作为泛型参数的默认边界。
但是有时我们想调用特定类或者接口的方法,这时我们可以通过定义泛型的边界来实现。GenericHolder2就是一个使用了自定义边界的例子,我们通过extends方法指定泛型参数继承了SimpleObject,此时T的边界就是SimpleObject。
从编译后的文件我们也能看到。当有了SimpleObject作为边界之后,我们就可以执行它的方法了,看下面的例子:

  1. public interface HasColor {
  2. java.awt.Color getColor();
  3. }
  1. public class WithColor<T extends HasColor> {
  2. private T item;
  3. public WithColor(T item) {
  4. this.item = item;
  5. }
  6. public T getItem() {
  7. return item;
  8. }
  9. public java.awt.Color getColor(){
  10. return item.getColor();
  11. }
  12. }

泛型类WithColor的类型变量T被指定需要实现HasColor接口,所以边界就是HasColor,这样就可以在getColor里面调用item的getColor方法了。
根据之前的经验,这个类反编译之后T会被HasColor替代,所以对其调用getColor肯定是可以的:

  1. public class WithColor
  2. {
  3. public WithColor(HasColor item)
  4. {
  5. this.item = item;
  6. }
  7. public HasColor getItem()
  8. {
  9. return item;
  10. }
  11. public Color getColor()
  12. {
  13. return item.getColor();
  14. }
  15. private HasColor item;
  16. }

继续我们的例子,我们添加一个类Coord:

  1. public class Coord {
  2. public int x,y,z;
  3. public int getCoords(){
  4. return x + y + z;
  5. };
  6. }

然后添加泛型类,该泛型类的类型参数需要继承Coord并实现HasColor:

  1. public class WithColorCoord<T extends Coord & HasColor> {
  2. T item;
  3. public WithColorCoord(T item) {
  4. this.item = item;
  5. }
  6. public T getItem() {
  7. return item;
  8. }
  9. java.awt.Color getColor(){
  10. return item.getColor();
  11. }
  12. int getX(){
  13. return item.x;
  14. }
  15. int getY(){
  16. return item.y;
  17. }
  18. int getZ(){
  19. return item.z;
  20. }
  21. int getCoord(){
  22. return item.getCoords();
  23. }
  24. }

T类型的item能够调用Coord和HasColor两者的方法,我们看一下反编译后的代码:

  1. public class WithColorCoord
  2. {
  3. public WithColorCoord(Coord item)
  4. {
  5. this.item = item;
  6. }
  7. public Coord getItem()
  8. {
  9. return item;
  10. }
  11. Color getColor()
  12. {
  13. return ((HasColor)item).getColor();
  14. }
  15. int getX()
  16. {
  17. return item.x;
  18. }
  19. int getY()
  20. {
  21. return item.y;
  22. }
  23. int getZ()
  24. {
  25. return item.z;
  26. }
  27. int getCoord()
  28. {
  29. return item.getCoords();
  30. }
  31. Coord item;
  32. }

编译器用Coord类替换了T,然后在每个调用Coord的方法上,就能直接使用了,而在调用HasColor的方法时,进行了强制类型转换,这种转换也是没有问题的。
继续增加一个接口:

  1. interface Weight {
  2. int weight();
  3. }

修改泛型类:

  1. public class Solid<T extends Coord & HasColor & Weight> {
  2. T item;
  3. public Solid(T item) {
  4. this.item = item;
  5. }
  6. public T getItem() {
  7. return item;
  8. }
  9. java.awt.Color color(){
  10. return item.getColor();
  11. }
  12. int getX(){
  13. return item.x;
  14. }
  15. int getY(){
  16. return item.y;
  17. }
  18. int getZ(){
  19. return item.z;
  20. }
  21. int weight(){
  22. return item.weight();
  23. }
  24. }

增加了weight接口之后,我们也可以在T上面调用weight的方法了,看编译后的代码:

  1. public class Solid
  2. {
  3. public Solid(Coord item)
  4. {
  5. this.item = item;
  6. }
  7. public Coord getItem()
  8. {
  9. return item;
  10. }
  11. Color color()
  12. {
  13. return ((HasColor)item).getColor();
  14. }
  15. int getX()
  16. {
  17. return item.x;
  18. }
  19. int getY()
  20. {
  21. return item.y;
  22. }
  23. int getZ()
  24. {
  25. return item.z;
  26. }
  27. int weight()
  28. {
  29. return ((Weight)item).weight();
  30. }
  31. Coord item;
  32. }

item还是被Coord替代,在使用weight和hasColor的部分,都是通过强制类型转换来做的。
也就是说,对于泛型,在没有给定边界的情况下,默认边界就是Object,在给定边界的情况下,边界是extends关键字后面的第一个类型T,注意当extends后面有多个类型例如的时候,如果其中一个是类,那么这个类必须放在第一个,后面的都是接口,否则就是一个编译时错误,如果都是接口,就没有顺序要求,但是顺序会影响擦除后的边界。
还有一点需要注意,在声明泛型类、泛型接口和泛型方法的类型变量时,都只能使用extends来指定边界,而不能使用super,这个要跟后面将要讲到的协变和逆变分清楚。

小结

这篇文章从泛型类和泛型方法反编译后的文件出发,分析了java泛型擦除的实现方式,我们应该知道编译器在泛型擦除的过程中起了很大的作用,它根据我们的泛型定义生成了擦除后的代码,为泛型找到了合适的边界。使用擦除来实现泛型同样导致了很多问题,例如不能使用instanceOf关键字,这些都可以通过其他方式来解决。
下一篇文章,我们会介绍泛型的另外一个问题—协变和逆变。