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