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

或者,可以標註整個方法,這個註解會關閉對方法中所有代碼的檢查。

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