Java 虚拟机(JVM,Java Virtual Machine)中并不存在泛型, Java 语言中的泛型只在程序源码中存在,在编译后的字节码文件(Class 文件)中, 全部泛型都被替换为原始类型,并且在相应的地方插入了 强制转型代码以及对 8 大基本类型的 自动装箱和拆箱。这样做的 主要目的是为了兼容以前的版本(泛型是在 JDK 1.5 之后才被引入 Java 中的,也就是说,在此之前 Java 并没有泛型的特性)。当然,利用这种方式实现泛型,所带来的不可避免的后果就是 执行性能的下降(Java 选择这样的泛型实现,是 出于当时语言现状的权衡,而不是语言先进性或者设计者水平不够原因,如果当时有充足的时间好好设计和实现,是 完全有可能做出更好的泛型系统的)。
1. 类型擦除
既然 JVM 中不存在泛型类型的对象,那么 Java 的泛型在 JVM 中又是如何定义的呢?答案是:类型擦除。
Java 的每个泛型类型都对应着一个相应的原始类型,原始类型用第一个限定的类型变量来替换, 如果没有给定限定就用 Object 替换。如:
泛型类型 | 原始类型 |
ArrayList<T> | ArrayList |
T | Object |
T extends Person & Comparable | Person |
类型擦除简单的来说,就是擦除原有的泛型类型,并用原始类型进行代替。具体外面可以看一个例子:
public class Person<T> {
private T information;
public Person() {
this(null);
}
public Person(T information) {
this.information = information;
}
public void setInformation(T information) {
this.information = information;
}
public T getInformation() {
return information;
}
}
对于上述的 Person<T> 类,类型擦除后的原始类型如下所示:
// 类型擦除后的Person类
public class Person {
// 泛型类型Person<T>被原始类型Person代替
// 类型变量T被 Object 代替
private Object information;
public Person() {
this(null);
}
public Person(Object information) {
this.information = information;
}
public void setInformation(Object information) {
this.information = information;
}
public Object getInformation() {
return information;
}
}
一个泛型类型(如 Person<String>),经类型擦除后,就变成了原始的类型(Person)。这样 JVM 就可以 “认识” 它了,这就解决了 JVM 中不存在泛型类型对象的限制。
2. 翻译泛型表达式
类型擦除解决了 JVM 中不存在泛型类型对象,但却又引出了一个新问题,请看下面的例子:
Person<String> person = new Person<>("泛型");
String information = person.getInformation();
这是一段很简单的代码,第一行实例化了一个 Person<String> 的对象,并在第二行读取了它的信息,将信息赋值给一个 String 类型的变量。整个代码看起来并没有什么特殊的地方,但如果你理解了类型擦除的机制,可能会对第二行的代码有些疑惑。
正如前面所说,类型擦除之后所有的类型变量均会被 Object 类型代替,即调用 person.getInformation () 得到的应该是一个 Object 类型的对象。而在第二行代码中我们直接让一个 String 类型的变量引用了它(没有经过强制类型转换)。
实际上,当程序调用泛型方法时,编译器会自动的帮我们插入强制类型转换(使用泛型数据的方法都是泛型方法)。在上述了例子中,擦除 getInformation () 的返回类型后会返回 Object 类型的对象,然后编译器自动的插入了 String 的强制类型转换。也就是说,编译器把这个方法调用翻译为两条虚拟机指令:
对原始方法 person.getInformation () 的调用(返回一个 Object 类型的对象)。
将返回的 Object 类型的对象强制转换为 String 类型。
除此之外,当存取一个泛型域时,编译器也会自动插入强制类型转换(如果这个域可以被外部访问到的话)。假设 Person 类的 information 变量是 public 的(这不是种好的编程风格),表达式:
String information = person.information;
也会在结果字节码中插入强制类型转换。
3. 桥方法
类型擦除还会带来一个问题,我们继续以 Proson 类为例子:
public class Person<T> {
private T information;
public void setInformation(T information) {
this.information = information;
}
public T getInformation() {
return information;
}
}
有一个类 MyPerson,它继承了 Person<String> 类,如果在 MyPerson 类中对 Person 类的方法进行重写,就会引起一些问题,例如:
public class MyPerson extends Person<String> {
// 在MyPerson类中,Person中继承的2个方法均重写。
// 返回值通过继承的泛型类确定为String。
@Override
public String getInformation() {
return super.getInformation();
}
// 参数通过继承的泛型类确定为String。
@Override
public void setInformation(String information) {
super.setInformation(information);
}
}
MyPerson 类继承了 Person<String> 类的两个方法,并对它们进行了覆盖重写。在 JVM 中,经过类型擦除后,以 setInformation 方法为例,Person 类中的是一个需要 Object 类型参数的方法,而 MyPerson 类中的是一个需要 String 类型参数的方法,它们显然不是同一个方法(详见类与对象 ——5. 方法)。
这里, 同样希望方法的调用具有多态性, 并调用最合适的那个方法。即如果是 MyPerson 类型的对象,就让他调用 MyPerson 类中的方法,而 Person<String> 类型的对象则调用 Person< String > 类中的方法。
要解决此问题,就需要编译器在 MyPerson 类中生成一个桥方法(bridge method):
public void setInformation(Object information) {
setInformation((String) information);
}
有了桥方法,我们就可以在泛型中实现多态:
Person<String> person = new MyPerson();
person.setInformation("泛型中的多态");
上述第二行代码会调用 MyPerson 类中的 setInformation 方法,实现了方法调用的多态性。
如果我们要进一步深究的话,这里还有一个问题,getInformation 方法怎么办(我们知道一个类中不允许存在多个仅有返回类型不同的同名方法)。如果继续利用桥方法,就会得到下面两个同名的方法,它们只有返回类型是不同的:
public String getInformation() {...}
public Object getInformation() {return getInformation()}
当然,我们不能编写这样的 Java 代码,但是,在 Java 虚拟机中,实际是通过参数类型和返回类型来确定一个方法的。也就是说,当编译器产生两个仅返回类型不同的方法字节码时,Java 虚拟机能够正确地处理这一情况。
最后,我们再来谈谈继承中的覆盖重写。桥方法不仅仅被用于泛型当中,在继承中,当一个方法覆盖另一个方法时,可以指定一个更严格的返回类型(详见面向对象程序设计 ——2.2.2 覆盖),这里其实也用到桥方法。具体原理与前面类似,便不再赘述。
4. 总结
JVM 中没有泛型,只有普通的类和方法。
在 JVM 中所有的类型参数都用它们的限定类型替换。
桥方法被合成来保持多态。
为保持类型安全性,必要时插人强制类型转换。
最后,需要注意的是,擦除的类其实仍然保留了一些泛型祖先的微弱记忆。例如, 擦除后原始的 Person 类知道它源于泛型类 Person<T>(但无法区分是由 Person< String > 构造的还是由 Person< Double > 构造的)。