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


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

注:二进制向后兼容性是明确写入《Java语言规范》中的对Java使用者的严肃承诺。
譬如一个在JDK 1.2中编译出来的Class文件,必须保证能够在JDK 12乃至以后的版本中也能够正常运行。

1. 类型擦除

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

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

泛型类型原始类型
ArrayList<T>ArrayList
TObject
T extends Person & ComparablePerson

关于 T extends Person & Comparable 的更多解释请点这

类型擦除简单的来说,就是擦除原有的泛型类型,并用原始类型进行代替。具体外面可以看一个例子:

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 的强制类型转换。也就是说,编译器把这个方法调用翻译为两条虚拟机指令:

  1. 对原始方法 person.getInformation() 的调用(返回一个 Object 类型的对象)。
  2. 将返回的 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> {

	@Override
	public String getInformation() {
		return super.getInformation();
	}
	
	@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 方法怎么办(我们知道一个类中不允许存在多个仅有返回类型不同同名方法,详见类与对象——5. 方法)。如果继续利用桥方法,就会得到下面两个同名的方法,它们只有返回类型是不同的:

	public String getInformation() {...}
	public Object getInformation() {return getInformation()}

当然,我们不能编写这样的 Java 代码,但是,在Java虚拟机中,实际是通过参数类型和返回类型来确定一个方法的。也就是说,当编译器产生两个仅返回类型不同的方法字节码时,Java虚拟机能够正确地处理这一情况。

最后,我们再来谈谈继承中的覆盖重写。桥方法不仅仅被用于泛型当中,在继承中,当一个方法覆盖另一个方法时,可以指定一个更严格的返回类型(详见面向对象程序设计——2.2.2 覆盖),这里其实也用到桥方法。具体原理与前面类似,便不再赘述。

4. 总结

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

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

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值