泛型编程(三)——泛型代码与虚拟机
Java虚拟机没有所谓的泛型类型——所有的类型都是原始类型。当你定义泛型类时,编译器生成一个同名的原始类型,直接移除泛型参数,使用泛型参数的地方将会被他们的边界类型代替(如果没有的话,就使用Object类型代替)。
比如,之前定义的Pair<T>类型将会使用下面的类定义代替
public class Pair
{
public Pair(Object first,Object second)
{
this.first = first;
this.second = second;
}
public Object getFirst(){return this.first;}
public Object getSecond() { return this.second; }
public void setFirst(Object newValue) { this.first = newValue; }
public void setSecond(Object newValue) { this.second = newValue; }
private Object first;
private Object second;
}
Pari类中的T是一个没有边界的变量,因此直接使用Object代替。编译器处理的结果就是一个常规类,就跟你学泛型类型之前定义类一样。
你的程序可能会使用多种类型的Pair类,但是经过转化之后,他们都使用同一个Pair类。这一点和C++不一样,在C++中,使用模板类实现泛型,模板类会为每个使用到的类型创建新的类。
原始类中替换的类型是第一个边界,如果没有边界,那就是 Object类型,比如上面的例子中,没有指定T的边界,因此直接使用Object替换。比如我们定义了另外一个类:
public class Interval<T extends Comparable & Serializable> implements Serializable
{
public Interval(T first,T second)
{
if(first.compareTo(second) <= 0) { lower = first; upper = second; }
else { lower = second; upper = first; }
}
...
private T lower;
private T upper;
}
那么Interval的原始类型看起来是这个样子的。
public class Interval implements Serializable
{
public Interval(Comparable first,Comparable second){...}
...
private Comparable lower;
private Comparable upper;
}
你可能会问,如果我换一种表达,会怎么变化呢,比如,如果我把定义换成class Interval<T extends Serializable & Comparable>会怎么样,答案是边以及会将T替换成Serializable,这是由编译器决定的,因此你应该将资源消耗更小的接口放在前面。
如何解释泛型语句
当你的程序调用泛型方法,编译器会在适当的地方加上类型转换。比如,考虑我们调用一个泛型类的方法
Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();
类型擦除机制会将getFirst的返回值变为Object类型。边以及自动添加语句将Object类型转换为Employee类型。当你获取泛型类的成员变量时,类似的语句也会被添加。比如,如果你获取Pair类中的first参数(不好的习惯,但是合法)
Employee buddy = buddies.getFirst();
类似的类型转换语句同样会被添加。
如何解释泛型方法
类型擦除也会发生在泛型方法中,方形方法的定义方法类似于
public static <T extends Comparable> T min(T[] a)
经过类型擦除后,方法定义为
public static Comparable min(Comparable[] a)
泛型范数T被边界类型Comparable替代。
方法的类型擦除会带来一些问题。考虑下面的问题
class DateInterval extends Pair<Date>
{
public void setSecond(Date second)
{
if(second.compareTo(geteFirst()) >= 0)
{
super.getSecond(second);
}
}
...
}
DateInterval继承自Pair<Date>类,我们想保证第二个更大。经过类型擦除后,
class DateInterval extends Pair
{
public void setSecond(Date second){...}
}
但问题在于,还有一个集成自Pair的setSecond类。
public void setSecond(Object second)
很显然他们是不同的类,但他们本不应该不同。考虑下面的语句
DateInterval interval = new DateInterval(...);
Pair<Date> pair = interval; // 不应该出现问题,赋值为子类对象
pair.setSecond(aDate);
我们的期望是,setSecond方法应该遵循多态的规则,调用合适的方法。应为它指向的是DateInterval对象,因此应该调用DateInterval的setSecond方法。为了解决这个问题,编译器会在DateInterval类中生成一个过渡方法,
public void setSecond(Object second) { setSecond((Date) second); }
为了解释这个语句的作用,让我们仔细地讲一下下面这条语句的执行流程。
pair.setSecond(aDate);
变量pari声明为Pair<Date>类型,他有一个函数setSecond(Object)。虚拟机执行他指向的对象的对应函数。他只想的对象是DateInterval类型的,那么编译器执行DateInterval.setSecond(Object)函数,这个函数就是上面合成的过渡函数,在内部,他执行了DateInterval.setSecond(Date),也就是我们想要的函数。
过渡函数可能会更奇怪,假设DateInterval方法重写了getSecond()方法。
class DateInterval extends Pair<Date>
{
public Date getSecond() { return (Date) super.getSecond().clone(); }
}
在类型擦除后,有两个getSecond()方法
Date getSecond() // 在DateInterval中定义
Object getSecond() // 在Pair中定义。
在java中,你没有办法编写这两个函数,java不允许两个参数类型相同的同名函数。但是,在java虚拟机中,两个同名函数有相同的参数,但返回值不同是可以的。因此,编译器可以根据返回值不同,生成不同的字节码,正确运行程序。
总之,在java中,如果你编写泛型类,需要注意:
- 虚拟机中没有泛型类,只有原始类型和方法
- 所有类型变量被边界类型代替。
- 为了防止类继承时的多态问题,会生成过渡方法。
- 会自动插入类型转换语句。