Java 泛型 02:泛型和虚拟机(类型擦除)

Java 虚拟机(JVM,Java Virtual Machine)中并不存在泛型, Java 语言中的泛型只在程序源码中存在,在编译后的字节码文件(Class 文件)中, 全部泛型都被替换为原始类型,并且在相应的地方插入了 强制转型代码以及对 8 大基本类型的 自动装箱和拆箱。这样做的 主要目的是为了兼容以前的版本(泛型是在 JDK 1.5 之后才被引入 Java 中的,也就是说,在此之前 Java 并没有泛型的特性)。当然,利用这种方式实现泛型,所带来的不可避免的后果就是 执行性能的下降(Java 选择这样的泛型实现,是 出于当时语言现状的权衡,而不是语言先进性或者设计者水平不够原因,如果当时有充足的时间好好设计和实现,是 完全有可能做出更好的泛型系统的)。

1. 类型擦除

既然 JVM 中不存在泛型类型的对象,那么 Java 的泛型在 JVM 中又是如何定义的呢?答案是:类型擦除

Java 的每个泛型类型都对应着一个相应的原始类型,原始类型用第一个限定的类型变量来替换, 如果没有给定限定就用 Object 替换。如:

泛型类型       原始类型
ArrayList

ArrayList

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),经类型擦除后,就变成了原始的类型(Person)。这样 JVM 就可以 “认识” 它了,这就解决了 JVM 中不存在泛型类型对象的限制。


2. 翻译泛型表达式

类型擦除解决了 JVM 中不存在泛型类型对象,但却又引出了一个新问题,请看下面的例子:

Person person = new Person<>("泛型");
String information = person.getInformation();

这是一段很简单的代码,第一行实例化了一个 Person 的对象,并在第二行读取了它的信息,将信息赋值给一个 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 {
	private T information; 

	public void setInformation(T information) {
		this.information = information;
	}
	
	public T getInformation() {
		return information;
	}
}

有一个类 MyPerson,它继承了 Person 类,如果在 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 类的两个方法,并对它们进行了覆盖重写。在 JVM 中,经过类型擦除后,以 setInformation 方法为例,Person 类中的是一个需要 Object 类型参数的方法,而 MyPerson 类中的是一个需要 String 类型参数的方法,它们显然不是同一个方法(详见类与对象 ——5. 方法)。

这里, 同样希望方法的调用具有多态性, 并调用最合适的那个方法。即如果是 MyPerson 类型的对象,就让他调用 MyPerson 类中的方法,而 Person 类型的对象则调用 Person< String > 类中的方法。

要解决此问题,就需要编译器在 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 覆盖),这里其实也用到桥方法。具体原理与前面类似,便不再赘述。

4. 总结

    JVM 中没有泛型,只有普通的类和方法。
    在 JVM 中所有的类型参数都用它们的限定类型替换。
    桥方法被合成来保持多态。
    为保持类型安全性,必要时插人强制类型转换。

最后,需要注意的是,擦除的类其实仍然保留了一些泛型祖先的微弱记忆。例如, 擦除后原始的 Person 类知道它源于泛型类 Person(但无法区分是由 Person< String > 构造的还是由 Person< Double > 构造的)。

你可能感兴趣的:(个人学习,Java,java,jvm,开发语言)