【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泛型轉換的事實:

  • 虛擬機中沒有泛型,只有普通的類和方法。
  • 所有的類型參數都用它們的限定類型替換。
  • 橋方法被合成來保持多態。
  • 爲保持類型安全性,必要時插入強制類型轉換。

捐贈

若你感覺讀到這篇文章對你有啓發,能引起你的思考。請不要吝嗇你的錢包,你的任何打賞或者捐贈都是對我莫大的鼓勵。

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