Java泛型02:泛型和虛擬機(類型擦除)

Java虛擬機(JVM,Java Virtual Machine)中並不存在泛型, Java 語言中的泛型只在程序源碼中存在,在編譯後的字節碼文件(Class 文件)中, 全部泛型都被替換爲原始類型,並且在相應的地方插入了 強制轉型代碼以及對8大基本類型的 自動裝箱和拆箱。這樣做的 主要目的是爲了兼容以前的版本(泛型是在 JDK 1.5 之後才被引入 Java 中的,也就是說,在此之前Java並沒有泛型的特性)。當然,利用這種方式實現泛型,所帶來的不可避免的後果就是 執行性能的下降(Java選擇這樣的泛型實現,是 出於當時語言現狀的權衡,而不是語言先進性或者設計者水平不夠原因,如果當時有充足的時間好好設計和實現,是 完全有可能做出更好的泛型系統的)。

1. 類型擦除

既然 JVM 中不存在泛型類型的對象,那麼 Java 的泛型在 JVM 中又是如何定義的呢?答案是:類型擦除

Java的每個泛型類型都對應着一個相應的原始類型,原始類型用第一個限定的類型變量來替換, 如果沒有給定限定就用Object 替換。如:

泛型類型 原始類型
ArrayList<T> ArrayList
T Object
T extends Person & Comparable Person

類型擦除簡單的來說,就是擦除原有的泛型類型,並用原始類型進行代替。具體外面可以看一個例子:

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 方法怎麼辦(我們知道一個類中不允許存在多個僅有返回類型不同的同名方法)。如果繼續利用橋方法,就會得到下面兩個同名的方法,它們只有返回類型是不同的:

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

當然,我們不能編寫這樣的 Java 代碼,但是,在Java虛擬機中,實際是通過參數類型和返回類型來確定一個方法的。也就是說,當編譯器產生兩個僅返回類型不同的方法字節碼時,Java虛擬機能夠正確地處理這一情況。

最後,我們再來談談繼承中的覆蓋重寫。橋方法不僅僅被用於泛型當中,在繼承中,當一個方法覆蓋另一個方法時,可以指定一個更嚴格的返回類型(詳見面向對象程序設計——2.2.2 覆蓋),這裏其實也用到橋方法。具體原理與前面類似,便不再贅述。

4. 總結

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

最後,需要注意的是,擦除的類其實仍然保留了一些泛型祖先的微弱記憶。例如, 擦除後原始的 Person 類知道它源於泛型類 Person< T >(但無法區分是由 Person< String > 構造的還是由 Person< Double > 構造的)。
 

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