【java基础(五十三)】类型擦除、翻译泛型

虚拟机没有泛型类型对象。所有对象都属于普通类。

类型擦除

无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(raw type)。原始类型的名字就是删去类型参数后的泛型类型名。擦除(erased)类型变量,并替换为限定类型(无限定的变量用Object)。

例如,Pair<T>的原始类型如下所示:

public class Pair {
	private Object first;
	private Object second;

	public Pair(Object first, Object second) {
		this.first = first;
		this.second = second;
	}

	public Object getFirst() {return first;}
	public Object getSecond() {return second;}

	public void setFirst(Object newValue) {first = newValue;}
	public void setSecond(Object newValue) {second = newValue;}
}

因为T是一个无限定的变量,所以直接用Object替换。

结果是一个普通的类,就好像泛型引入Java语言之前已经实现的那样。

在程序中可以包含不同类型的Pair,例如,Pair<String>Pair<LocalDate>。而擦除类型后就变成原始的Pair类型了。

原始类型用第一个限定的类型变量来替换,如果没有给定限定就用Object替换。例如,类Pair<T>中的类型变量没有显示的限定,因此,原始类型用Object替换T。假定声明了一个不同的类型。

public class Interval<T extends Comparable & Serializable> implements Serializable {
	private T lower;
	private T upper;
	...
	public Inteval(T first, T second) {
		if (first.compareTo(second) <= 0) {
			lower = first;
			upper = second;
		} else {
			lower = second;
			upper = first;
		}
	}
}

原始类型Interval如下:

public class Interval implements Serializable {
	private Comparable lower;
	private Comparable upper;
	...
	public Interval(Comparable first, Comparable second){...}
}

翻译泛型表达式

当程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换。如:

Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();

擦除getFirst的返回类型后将返回Object类型,编译器自动插入Employee的强制类型转换。也就是说,编译器把这个方法调用翻译为两条虚拟机指令:

  • 对原始方法Pair.getFirst的调用。
  • 将返回的Object类型强制转换为Employee类型。

当存取一个泛型域时也要插入强制类型转换。假设Pair类的first域和second域都是共有的。表达式:

Employee buddy = buddies.first;

也会在结果字节码中插入强制强制类型转换。

翻译泛型方法

类型擦除也会出现在泛型方法中。程序员通常认为这个泛型方法:

public static <T extends Comparable> T min(T[] a)

是一个完整的方法族,而擦除类型之后,只剩下一个方法:

public static Comparable min(Comparable[] a);

注意,类型参数T已经被擦除了,只留下了限定类型Comparable

方法的擦除带来了两个复杂问题。如:

class DateInterval extends Pair<LocalDate> {
	public void setSecond(LocalDate second) {
		if (second.compareTo(getFirst()) >= 0)
			super.setSecond(second);
	}
}

一个日期区间是一对LocalDate对象,并且需要覆盖这个方法来确保第二个永远不小于第一个值。这个类擦除后:

class DateInterval extends Pair {
	public void setSecond(LocalDate second){...}
}

令人感到奇怪的是,存在另一个从Pair继承的setSecond方法,即

public void setSecond(Object second)

这显然是一个不同的方法,因为它有一个不同的类型参数Object,而不是LocalDate。然而,不应该不一样。考虑下面的语句序列:

DateInterval interval = new DateInterval(...);
Pair<LocalDate> pair = interval;
pair.setSecond(aDate);

这里,希望对setSecond的调用具有多态性,并调用最合适的那个方法。由于pair引用DateInterval对象,所以应该调用DateInterval.setSecond。问题在于类型擦除与多态发生了冲突。要解决这个问题,就需要编译器在DateInterval类中生成一个桥方法(bridge method)。

public void setSecond(Object second){
	setSecond((Date)second);
}

想要了解它的工作过程,请仔细地跟踪下列语句的执行:

pair.setSecond(aDate);

变量pair已经声明为类型Pair<LocalDate>,并且这个类型只有一个简单的方法叫setSecond,即setSecond(Object)。虚拟机用pair引用的对象调用这个方法。这个对象是DateInterval类型的,因而将会调用DateInterval.setSecond(Object)方法。这个方法是合成的桥方法。它调用DateInterval.setSecond(Date),这正是我们所期望的操作效果。

桥方法可能会变得十分奇怪。假设DateInterval方法也覆盖了getSecond方法:

class DateInterval extends Pair<LocalDate> {
	public LocalDate getSecond() {
		return (Date)super.getSecond().clone();
	}
}

DateInterval类中,有两个getSecond方法:

LocalDate getSecond();
Object getSecond();

不能这样编写Java代码(具有相同参数类型的两个方法是不合法的)。它们都没有参数。但是,在虚拟机中,用参数类型和返回类型确定一个方法。因此,编译器可能产生两个仅返回类型不同的方法字节码,虚拟机能够正确地处理这一情况。

总之,需要记住有关Java泛型转换的事实:

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

捐赠

若你感觉读到这篇文章对你有启发,能引起你的思考。请不要吝啬你的钱包,你的任何打赏或者捐赠都是对我莫大的鼓励。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章