了解泛型擦除吗?知道类型擦除会造成多态的冲突吗?如何解决?

泛型的代码只存在于编译阶段,在进入JVM之前,与泛型相关的信息会被擦除掉,称之为类型擦除。

  • 无限制类型擦除:当在类的定义时没有进行任何限制,那么在类型擦除后将会被替换成Object,例如<T><?> 都会被替换成Object。
  • 有限制类型擦除:当类定义中的参数类型存在上下限(上下界),那么在类型擦除后就会被替换成类型参数所定义的上界或者下界,例如<? extend Person>会被替换成Person,而<? super Person> 则会被替换成Object。

泛型的桥接方法

类型擦除会造成多态的冲突,而JVM解决方法就是桥接方法

举例

现在有这样一个泛型类:

class Pair<T> { 
 private T value; 
 public T getValue() { 
 return value; 
 } 
 public void setValue(T value) { 
 this.value = value; 
 } 
}

然后一个子类继承它

class DateInter extends Pair<Date> { 
 @Override 
 public void setValue(Date value) { 
 super.setValue(value); 
 } 
 @Override 
 public Date getValue() { 
 return super.getValue(); 
 } 
}

在这个子类中,设定父类的泛型类型为Pair<Date>,在子类中,覆盖了父类的两个方法,原意是这样的:将父类的泛型类型限定为Date,那么父类里面的两个方法的参数都为Date类型。

public Date getValue() { 
 return value; 
} 
public void setValue(Date value) { 
 this.value = value; 
}

实际上,类型擦除后,父类的的泛型类型全部变为了原始类型Object,所以父类编译之后会变成下面的样子:

class Pair { 
 private Object value; 
 public Object getValue() { 
 return value; 
 } 
 public void setValue(Object value) { 
 this.value = value; 
 } 
} 

再看子类的两个重写的方法的类型:setValue方法,父类的类型是Object,而子类的类型是Date,参数类型不一样,这如果实在普通的继承关系中,根本就不会是重写,而是重载。 在一个main方法测试一下:

public static void main(String[] args) throws ClassNotFoundException { 
 DateInter dateInter = new DateInter(); 
 dateInter.setValue(new Date()); 
 dateInter.setValue(new Object()); //编译错误 
}

如果是重载,那么子类中两个setValue方法,一个是参数Object类型,一个是Date类型,可是根本就没有这样的一个子类继承自父类的Object类型参数的方法。所以说,确实是重写了,而不是重载了。

为什么这样?

原因是这样的,传入父类的泛型类型是Date,Pair<Date>,本意是将泛型类变为如下:

class Pair { 
 private Date value; 
 public Date getValue() { 
 return value; 
 } 
 public void setValue(Date value) { 
 this.value = value; 
 } 
}

然后在子类中重写参数类型为Date的两个方法,实现继承中的多态。

可是由于种种原因,虚拟机并不能将泛型类型变为Date,只能将类型擦除掉,变为原始类型Object。这样,原来是想进行重写,实现多态,可是类型擦除后,只能变为了重载。这样,类型擦除就和多态有了冲突。于是JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。

原理

用javap -c className的方式反编译下DateInter子类的字节码,结果如下:

class com.tao.test.DateInter extends com.tao.test.Pair<java.util.Date> { 
 com.tao.test.DateInter(); 
 Code: 
 0: aload_0 
 1: invokespecial #8 // Method com/tao/test/Pair."<init>":()V 
 4: return 
 public void setValue(java.util.Date); //我们重写的setValue方法 
 Code: 
 0: aload_0 
 1: aload_1 
 2: invokespecial #16 // Method com/tao/test/Pair.setValue:(Ljava/lang/Object;)V 
 5: return 
 public java.util.Date getValue(); //我们重写的getValue方法 
 Code: 
 0: aload_0 
 1: invokespecial #23 // Method com/tao/test/Pair.getValue:()Ljava/lang/Object; 
 4: checkcast #26 // class java/util/Date 
 7: areturn 
 public java.lang.Object getValue(); //编译时由编译器生成的桥方法 
 Code: 
 0: aload_0 
 1: invokevirtual #28 // Method getValue:()Ljava/util/Date 去调用我们重写的getValue方法; 
 4: areturn 
 public void setValue(java.lang.Object); //编译时由编译器生成的桥方法 
 Code: 
 0: aload_0 
 1: aload_1 
 2: checkcast #26 // class java/util/Date 
 5: invokevirtual #30 // Method setValue:(Ljava/util/Date; 去调用我们重写的setValue方法)V 
 8: return 
}

从编译的结果来看,本意重写setValue和getValue方法的子类,但是反编译后竟然有4个方法,其实最后的两个方法,就是编译器自己生成的桥方法。可以看到桥方法的参数类型都是Object,也就是说,子类中真正覆盖父类两个方法的就是这两个我们看不到的桥方法。而在setvalue和getValue方法上面的@Oveerride只不过是假象。而桥方法的内部实现,就只是去调用自己重写的那两个方法。

所以,虚拟机巧妙的使用了桥方法,来解决了类型擦除和多态的冲突。

并且,还有一点也许会有疑问,子类中的桥方法Object getValue()和Date getValue()是同时存在的,可是如果是常规的两个方法,他们的方法签名是一样的,如果是我们自己编写Java代码,这样的代码是无法通过编译器的检查的(返回值不同不能作为重载的条件),但是虚拟机却是允许这样做的,因为虚拟机通过参数类型和返回类型来确定一个方法,所以编译器为了实现泛型的多态允许自己做这个看起来“不合法”的事情,然后交给虚拟机去区别

作者:程序员Seven原文地址:https://www.cnblogs.com/seven97-top/p/18865103

%s 个评论

要回复文章请先登录注册