关于Java泛型擦除后,继承一个泛型类带来的问题
有如下泛型类Pair:
public class Pair<T>
{
private T second;
private T first;
public Pair()
{
first = null;
second = null;
}
public Pair(T first, T second)
{
this.first = first;
this.second =second;
}
public T getFirst()
{
return first;
}
public T getSecond()
{
return second;
}
public void setFirst(T newValue)
{
first = newValue;
}
public void setSecond(T newValue)
{
second = newValue;
}
}
编译器会进行类型擦除,将其变为下面的普通类:
public class Pair
{
private Object second;
private Object first;
public Pair()
{
first = null;
second = null;
}
public Pair(Object first, Object second)
{
this.first = first;
this.second =second;
}
public Object getFirst()
{
return first;
}
public Object getSecond()
{
return second;
}
public void setFirst(Object newValue)
{
first = newValue;
}
public void setSecond(Object newValue)
{
second = newValue;
}
}
现在,有一个继承自泛型类Pair的类DateInterval:
class DateInterval extends Pair<Date>
{
public void setSecond(Date second)
{
if (second.compareTo(getFirst()) >= 0)
super.setSecond(second);
...
}
}
这个类重写了Pair的方法setSecond
, 那么在类型擦除的时候,这个类其实是继承自类Pair:
class DateInterval extends Pair
{
public void setSecond(Date second)
{
if (second.compareTo(getFirst()) >= 0)
super.setSecond(second);
...
}
}
注意,这里的setSecond方法的参数还依然是Date类型
的,不会被擦除,这是因为编译器擦除类型参数的时候擦除的是泛型类所在的类型参数,这个DateInterval类不是泛型类,方法setSecond的参数Date second也不是泛型参数。
那么,如果java编译器不做任何特殊处理的话,这个本来想覆盖Pair中setSecond方法的类就变成了一个DateInterval类中的新方法了,因为两个方法的签名不同,一个是Object参数,一个是Date参数:
public class Pair
{
...
public void setSecond(Object newValue)
{
second = newValue;
}
}
class DateInterval extends Pair
{
public void setSecond(Date second)
{
if (second.compareTo(getFirst()) >= 0)
super.setSecond(second);
...
}
}
那么当我们想用到多态的时候:
DateInterval interval = new DateInterval(. . .);
Pair<Date> pair = interval; // OK--assignment to superclass
pair.setSecond(aDate);
像上面这样,interval向上转型为了父类Pair,那么在调用pair.setSecond的时候,应该调用pair的实际类型DateInterval中的setSecond(Date second)方法,
但由于类型擦除,DateInterval中的setSecond(Date second)方法是新方法,没有重写父类Pair中的任何方法,那么就不会启动多态机制,pair.setSecond(aDate)只会调用父类Pair中的setSecond(Object)方法。
这显然不是我们想要的,为了解决这种问题,Java编译器会在DateInterval类中加入一个桥接方法 setSecond(Object newValue) 来达到覆盖擦除类型参数后的父类Pair中同名方法的作用:
class DateInterval extends Pair
{
public void setSecond(Date second)
{
if (second.compareTo(getFirst()) >= 0)
super.setSecond(second);
...
}
//编译器加入的桥接方法,使其可以实现多态,并调用正确的,上面高亮的setSecond(Date second)方法
public void setSecond(Object second)
{
setSecond((Date) second);
}
}
那么下面这段代码的执行顺序:
pair.setSecond(aDate)
-
首先,pair引用的实际类型是DateInterval,多态机制会调用DateInterval.setSecond(Object second)
-
DateInterval.setSecond(Object second)是桥接方法,内部会调用真正的处理方法DateInterval.setSecond(Date second)
当一个继承自泛型类的类想覆盖的方法是一个get方法时,情况就更奇怪了,如下代码:
class DateInterval extends Pair<Date>
{
public Date getSecond()
{
return (Date) super.getSecond().clone();
}
...
}
这个DateInterval覆盖了父类Pair的方法getSecond,由于泛型类类参数的擦除,Pair变为了Pair,getSecond返回值也变为了Object:
public class Pair
{
...
public Object getSecond()
{
return second;
}
...
}
那么跟前面讲得情况相同,DateInterval类中原本想覆盖的方法public Date getSecond() 变为了一个DateInterval类中新定义的方法,没有了多态机制。
同样,Java会在编译的时候加入一个桥接方法:
class DateInterval extends Pair<Date>
{
public Date getSecond()
{
return (Date) super.getSecond().clone();
}
//桥接方法
public Object getSecond()
{
...
}
...
}
由于真正的Java代码是不能只靠方法的返回类型不同区分一个方法的,但是Java的字节码是可以将这种情况视为两个不同的方法的,所以上面的这种Java桥接方法可以工作。
You could not write Java code like that; it would be illegal to have two methods with the same parameter types—here, with no parameters. However, in the virtual machine, the parameter types and the return type specify a method. Therefore, the compiler can produce bytecodes for two methods that differ only in their return type, and the virtual machine will handle this situation correctly.
这种方式不限于泛型类,像下面的代码:
public class Employee implements Cloneable
{
public Employee clone() throws CloneNotSupportedException { ... }
}
Employee类实现了Cloneable接口的Object clone()方法,但是它实现的是一个返回更具体类型Employee的方法,这在Java中是允许的。
而且也是通过Java在编译时加入一个同名的,返回Object类型的clone方法实现的:
public class Employee implements Cloneable
{
public Employee clone() throws CloneNotSupportedException { ... }
//桥接方法,此方法的方法体是调用上面返回Employee类型的clone方法。
public Object clone() {…}
}
总结
- 在Java虚拟机中是没有泛型的,只有普通的类和方法
- 所有的类型参数都被替换到它的边界上
- 桥接方法是被用来保留多态机制的
- 编译器会在适当的地方加入强制类型转换的代码,以便达到类型安全的目的