Java核心技术:泛型程序设计——泛型代码和虚拟机

Java核心技术:泛型程序设计——泛型代码和虚拟机


虚拟机没有泛型类对象——所有对象都属于普通类。所以泛型类会在编译时被翻译成普通类。

类型擦除

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

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语言之前的实现是类似的。
**原始类型用第一个限定的类型变量来替换,如果没有给定限定用Object替换。**如果声明了一个不同的类型:

public class Interval(T extends Comparable & Serizalizable> implements Serializable
{
	private T lower;
	private T upper;
	...
	public Interval(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) { ... }
}

如果将Interavel切换限定为:class Interavel<T extends Serializable & Comparable>,原始类型将用Serializable替换T,编译器在必要时要向Comparable插入强制类型转换。为了提高效率,应该将标签(tagging)接口放在便捷列表的末尾。

翻译泛型表达式

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

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

当存取一个泛型域时也要插入强制类型转换。例如:

Employee buddy = (Employee) 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);
		}
	}
	...
}

DateInterval类中,我们想覆盖PairsetSecond方法来确保第二个值永远不小于第一个值。DateInterval类擦除后变成:

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

这个时候问题就出来了,我们发现DateInterval类中的setSecond方法参数与Pair类中的setSecond方法参数不一样。这样,DateInterval类就有两个setSecond方法:

// 自己的
public void setSecond(LocalDate second) { ... }
// 从Pair父类中继承的
public void setSecond(Object second) { ... }

我们原本想通过在DateInterval类中重写父类PairsetSecond方法来实现继承多态性,可是类型擦除后,变成了重载。考虑一下代码:

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

pairPair<LocalDate>类型,当在pair对象上调用setSecond(LocalDate.now())时,会执行PairsetSecond(Object second)方法,由于pair引用DateInterval对象,如果DateInterval类的setSecond方法覆盖了父类的该方法,应该调用DateInterval.setSecond(LocalDate)。但DateInerval没有重写而是重载了Pair.setSecond(Object)方法,也就是说是类型擦除与多态发送了冲突。(更多说明参见:【java】–泛型-类型擦除与多态的冲突和解决方法_Java_qfzhangwei的专栏-CSDN博客
要解决这个问题,就需要编译器在DateInterval类中生成一个桥方法(bridge method):

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

(编译器在编译阶段自动生成)
跟踪下列语句的执行:

pair.setSecond(LocalDate.now());

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

class DateInterval extends Pair<LocalDate>
{
	public LocalDate getSecond() { ... }
	...
}

DateInterval类中,有两个getSecond方法:

LocalDate getSecond() // defined in DateInterval
Object getSecond() // overrides the method defined in Pair to call the first method

我们不能这样编写Java代码,它们有相同的方法名称且都没有参。但是,在虚拟机中,是用参数类型和返回类型确定一个方法的。因此,编译可能产生两个仅返回类型不同的方法字节码,虚拟机能够正确地处理这一情况。
桥方法不仅用于泛型类型。在一个方法覆盖另一个方法时可以指定一个更严格的返回类型。例如:

public class Employee implements Cloneable
{
	public Employee clone() throws CloneNotSupportedException { ... }

Object.cloneEmployee.clone方法被说成具有协变的返回类型(covariant return types)。实际上,Employee类有两个克隆方法:

Employee clone() // defaine above
Object clone() // synthesized bridge method, overrides Object.clone

合成的桥方法调用了新定义的方法。
总之,需要记住有关Java泛型转换的事实

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

调用遗留代码

设计Java泛型类型时,主要目标是允许泛型代码和遗留代码之间能互操作。
下面示例中,要想设置一个JSlider标签,可以使用方法:

void setLabelTable(Dictionary table)

这里的Dictionary是一个原始类型,因为实现JSlider类时Java中还不存在泛型。不过,填充Dictionary时,要使用泛型类型。

Dictionary<Integer, Component> labelTable = new Hashtable<>();
lableTable.put(0, new JLable(new ImageIcon("nine.gif")));
lableTable.put(20, new JLable(new ImageIcon("ten.gif")));
...

Dictionary<Integer, Component>对象传递给setLabelTable时,编译器会发送一个警告。

slider.setLabelTable(labelTable); // Warning

毕竟,编译器无法确定setLabelTable可能会对Dictionary对象做什么操作。这个方法可能会用字符串替换所有的关键字。这就打破了关键字类型为整数(Integer)的承诺,未来的操作有可能会产生强制类型转换的异常。
这个警告对操作不会产生什么影响,最多考虑一下JSlider有可能用Dictionary·对象做什么就可以了。这里十分清楚,JSlider只阅读这个信息,因此可以忽略这个警告。
在查看了警告之后,可以利用注解(annotation)使之消失。注解必须放在生成这个警告的代码所在的方法之前,如下:

@SuppressWarning("unchecked")
Dictionary<Integer, Components> labelTable = slider.getLabelTable(); // No warning

或者,可以标注整个方法,这个注解会关闭对方法中所有代码的检查。

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