Java 範型實現原理

一、Java泛型的實現方法:類型擦除

在最開始的時候已經簡單介紹了一下,現在在回顧一下:

泛型思想早在C++語言的模板(Templates)中就開始生根發芽,在Java語言處於還沒有出現泛型的版本時,只能通過Object是所有類型的父類和類型強制轉換兩個特點的配合來實現類型泛化。例如在哈希表的存取中,JDK 1.5之前使用HashMap的get()方法,返回值就是一個Object對象,由於Java語言裏面所有的類型都繼承於java.lang.Object,那Object轉型成任何對象都是有可能的。但是也因爲有無限的可能性,就只有程序員和運行期的虛擬機才知道這個Object到底是個什麼類型的對象。在編譯期間,編譯器無法檢查這個Object的強制轉型是否成功,如果僅僅依賴程序員去保障這項操作的正確性,許多ClassCastException的風險就會被轉嫁到程序運行期之中。
  泛型技術在C#和Java之中的使用方式看似相同,但實現上卻有着根本性的分歧,C#裏面泛型無論在程序源碼中、編譯後的IL中(Intermediate Language,中間語言,這時候泛型是一個佔位符)或是運行期的CLR中都是切實存在的,List<int>與List<String>就是兩個不同的類型,它們在系統運行期生成,有自己的虛方法表和類型數據,這種實現稱爲類型膨脹,基於這種方法實現的泛型被稱爲真實泛型。
  Java語言中的泛型則不一樣,它只在程序源碼中存在,在編譯後的字節碼文件中,就已經被替換爲原來的原生類型(Raw Type,也稱爲裸類型)了,並且在相應的地方插入了強制轉型代碼,因此對於運行期的Java語言來說,ArrayList<int>與ArrayList<String>就是同一個類。所以說泛型技術實際上是Java語言的一顆語法糖,Java語言中的泛型實現方法稱爲類型擦除,基於這種方法實現的泛型被稱爲僞泛型。


正確理解泛型概念的首要前提是理解類型擦出(type erasure)Java中的泛型基本上都是在編譯器這個層次來實現的。在生成的Java字節碼中是不包含泛型中的類型信息的。使用泛型的時候加上的類型參數,會在編譯器在編譯的時候去掉。這個過程就稱爲類型擦除。如在代碼中定義的List<object>和List<String>等類型,在編譯後都會編程List。JVM看到的只是List,而由泛型附加的類型信息對JVM來說是不可見的。Java編譯器會在編譯時儘可能的發現可能出錯的地方,但是仍然無法避免在運行時刻出現類型轉換異常的情況。類型擦除也是Java的泛型實現方法與C++模版機制實現方式之間的重要區別。

很多泛型的奇怪特性都與這個類型擦除的存在有關,包括:

  • 泛型類並沒有自己獨有的Class類對象。比如並不存在List<String>.class或是List<Integer>.class,而只有List.class。
  • 靜態變量是被泛型類的所有實例所共享的。對於聲明爲MyClass<T>的類,訪問其中的靜態變量的方法仍然是 MyClass.myStaticVar。不管是通過new MyClass<String>還是new MyClass<Integer>創建的對象,都是共享一個靜態變量。
  • 泛型的類型參數不能用在Java異常處理的catch語句中。因爲異常處理是由JVM在運行時刻來進行的。由於類型信息被擦除,JVM是無法區分兩個異常類型MyException<String>和MyException<Integer>的。對於JVM來說,它們都是 MyException類型的。也就無法執行與異常對應的catch語句。

可以通過兩個簡單的例子,來證明java泛型的類型擦除。

例1、

public class Test4 {
	public static void main(String[] args) {
		ArrayList<String> arrayList1=new ArrayList<String>();
		arrayList1.add("abc");
		ArrayList<Integer> arrayList2=new ArrayList<Integer>();
		arrayList2.add(123);
		System.out.println(arrayList1.getClass()==arrayList2.getClass());
	}
}
在這個例子中,我們定義了兩個ArrayList數組,不過一個是ArrayList<String>泛型類型,只能存儲字符串。一個是ArrayList<Integer>泛型類型,只能存儲整形。最後,我們通過arrayList1對象和arrayList2對象的getClass方法獲取它們的類的信息,最後發現結果爲true。說明泛型類型String和Integer都被擦除掉了,只剩下了原始類型


例2、

public class Test4 {
	public static void main(String[] args) throws IllegalArgumentException, SecurityException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
		ArrayList<Integer> arrayList3=new ArrayList<Integer>();
		arrayList3.add(1);//這樣調用add方法只能存儲整形,因爲泛型類型的實例爲Integer
		arrayList3.getClass().getMethod("add", Object.class).invoke(arrayList3, "asd");
		for (int i=0;i<arrayList3.size();i++) {
			System.out.println(arrayList3.get(i));
		}
	}
在程序中定義了一個ArrayList泛型類型實例化爲Integer的對象,如果直接調用add方法,那麼只能存儲整形的數據。不過當我們利用反射調用add方法的時候,卻可以存儲字符串。這說明了Integer泛型實例在編譯之後被擦除了,只保留了原型類型


二、類型擦除後保留的原始類型

在上面,兩次提到了原始類型,什麼是原始類型?原始類型(raw type)就是擦除去了類型參數的泛型類型的名字。無論何時定義一個泛型類型,相應的原始類型都會被自動地提供。類型變量被擦除(crased),並使用其限定類型(無限定的變量用Object)替換。

比如:

例3:

public class Pair<T> {
	private T first;
	private T second;
	public Pair() {
		first=null;
		second=null;
	}
	public  Pair(T first,T second){
		this.first=first;
		this.second=second;
	}
	public T getFirst() {
		return first;
	}
	public void setFirst(T first) {
		this.first = first;
	}
	public T getSecond() {
		return second;
	}
	public void setSecond(T second) {
		this.second = second;
	}
}
Pair<T>的原始類型爲:

public class Pair {
	private Object first;
	private Object  second;
	public Pair() {
		first=null;
		second=null;
	}
	public  Pair(Object  first,Object  second){
		this.first=first;
		this.second=second;
	}
	public Object  getFirst() {
		return first;
	}
	public void setFirst(Object  first) {
		this.first = first;
	}
	public Object  getSecond() {
		return second;
	}
	public void setSecond(Object  second) {
		this.second = second;
	}
}
因爲在Pair<T>中,T是一個無限定的類型變量,所以用Object替換。其結果就是一個普通的類,如同泛型加入java變成語言之前已經實現的那樣。在程序中可以包含不同類型的Pair,如Pair<String>或Pair<Integer>,但是,擦除類型後它們就成爲原始的Pair類型了。

從上面的那個例2中,我們也可以明白ArrayList<Integer>被擦除類型後,原始類型也變成了Object,所以通過反射我們就可以存儲字符串了。


如果類型變量有限定,那麼原始類型就用第一個邊界的類型變量來替換。

比如Pair這樣聲明

例4:

public class Pair<T extends Comparable& Serializable> {
那麼原始類型就是Comparable

注意:

如果Pair這樣聲明public class Pair<T extends Serializable&Comparable> ,那麼原始類型就用Serializable替換,而編譯器在必要的時要向Comparable插入強制類型轉換。爲了提高效率,應該將標籤(tagging)接口(即沒有方法的接口)放在邊界限定列表的末尾。

三、翻譯泛型表達式

因爲類型擦除的問題,所以所有的泛型類型變量最後都會被替換爲原始類型。這樣就引起了一個問題,既然都被替換爲原始類型,那麼爲什麼我們在獲取的時候,不需要進行強制類型轉換呢?就比如ArrayList<String>存儲了一些字符串,而它被類型擦除後,原始類型爲Object,那麼爲什麼我們獲取的時候,可以直接以String str=arrayList.get(1)這樣的形式獲取呢?

這是因爲當程序調用這些泛型方法的時候,如果返回類型被擦除了,編譯器會自動插入強制類型轉換。

以上面例3的Pair<T>舉例說明:

例5:

Pair<Date> pair=new Pair<Date>();
	    Date date=pair.getFirst();
getFirst的讀出將返回Object類型。編譯器會自動插入Date的強制類型轉換,而不需要我們自己去強轉。

當存取一個泛型域時也會自動插入強制類型轉換。假設Pair類的first和second域都是public的,那麼,表達式:

Date date=pair.first
也會自動地在結果字節碼中插入強制類型轉換。

四、翻譯泛型方法(重點)

類型擦除也會出現在泛型方法中。

1、先檢查,在編譯

理解泛型方法,首先要明白java編譯器會先檢查代碼中泛型的類型,然後再進行類型擦除,在進行編譯的。

舉個例子說明:

public static  void main(String[] args) {
		ArrayList<String> arrayList=new ArrayList<String>();
		arrayList.add("123");
		arrayList.add(123);//編譯錯誤
	}
在上面的程序中,使用add方法添加一個整形,在eclipse中,直接就會報錯,說明這就是在編譯之前的檢查。因爲如果是在編譯之後檢查,類型擦除後,原始類型爲Object,是應該運行任意引用類型的添加的。可實際上卻不是這樣,這恰恰說明了關於泛型變量的使用,是會在編譯之前檢查的。

2、類型擦除與多態的衝突和解決方法

現在有這樣一個泛型類:

class Pair<T> {
	private T value;
	public T getValue() {
		return value;
	}
	public void setValue(T value) {
		this.value = value;
	}
}

然後我們想要一個子類繼承它

class DateInter extends Pair<Date> {
	@Override
	public void setValue(Date value) {
		super.setValue(value);
	}
	@Override
	public Date getValue() {
		return super.getValue();
	}
}
在這個子類中,我們設定父類的泛型類型爲Pair<Date>,在子類中,我們覆蓋了父類的兩個方法,我們的原意是這樣的:

將父類的泛型類型限定爲Date,那麼父類裏面的兩個方法的參數都爲Date類型:“

	public Date getValue() {
		return value;
	}
	public void setValue(Date value) {
		this.value = value;
	}
 
所以,我們在子類中重寫這兩個方法一點問題也沒有,實際上,從他們的@Override標籤中也可以看到,一點問題也沒有,實際上是這樣的嗎?


分析:

實際上,類型擦除後,父類的的泛型類型全部變爲了原始類型Object,所以父類編譯之後會變成下面的樣子:

class Pair {
	private Object value;
	public Object getValue() {
		return value;
	}
	public void setValue(Object  value) {
		this.value = value;
	}
}
再看子類的兩個重寫的方法的類型:

        @Override
	public void setValue(Date value) {
		super.setValue(value);
	}
	@Override
	public Date getValue() {
		return super.getValue();
	}
先來分析setValue方法,父類的類型是Object,而子類的類型是Date,參數類型不一樣,這如果實在普通的繼承關係中,根本就不會是重寫,而是重載。
我們在一個main方法測試一下:
public static void main(String[] args) throws ClassNotFoundException {
		DateInter dateInter=new DateInter();
		dateInter.setValue(new Date());                
                dateInter.setValue(new Object());//編譯錯誤
 }
如果是重載,那麼子類中兩個setValue方法,一個是參數Object類型,一個是Date類型,可是我們發現,根本就沒有這樣的一個子類繼承自父類的Object類型參數的方法。所以說,卻是是重寫了,而不是重載了。


爲什麼會這樣呢?

原因是這樣的,我們傳入父類的泛型類型是Date,Pair<Date>,我們的本意是將泛型類變爲如下:

class Pair {
	private Date value;
	public Date getValue() {
		return value;
	}
	public void setValue(Date value) {
		this.value = value;
	}
}
然後再子類中重寫參數類型爲Date的那兩個方法,實現繼承中的多態。

可是由於種種原因,虛擬機並不能將泛型類型變爲Date,只能將類型擦除掉,變爲原始類型Object。這樣,我們的本意是進行重寫,實現多態。可是類型擦除後,只能變爲了重載。這樣,類型擦除就和多態有了衝突。JVM知道你的本意嗎?知道!!!可是它能直接實現嗎,不能!!!如果真的不能的話,那我們怎麼去重寫我們想要的Date類型參數的方法啊。

於是JVM採用了一個特殊的方法,來完成這項功能,那就是橋方法

首先,我們用javap -c className的方式反編譯下DateInter子類的字節碼,結果如下:

class com.tao.test.DateInter extends com.tao.test.Pair<java.util.Date> {
  com.tao.test.DateInter();
    Code:
       0: aload_0
       1: invokespecial #8                  // Method com/tao/test/Pair."<init>"
:()V
       4: return

  public void setValue(java.util.Date);  //我們重寫的setValue方法
    Code:
       0: aload_0
       1: aload_1
       2: invokespecial #16                 // Method com/tao/test/Pair.setValue
:(Ljava/lang/Object;)V
       5: return

  public java.util.Date getValue();    //我們重寫的getValue方法
    Code:
       0: aload_0
       1: invokespecial #23                 // Method com/tao/test/Pair.getValue
:()Ljava/lang/Object;
       4: checkcast     #26                 // class java/util/Date
       7: areturn

  public java.lang.Object getValue();     //編譯時由編譯器生成的巧方法
    Code:
       0: aload_0
       1: invokevirtual #28                 // Method getValue:()Ljava/util/Date 去調用我們重寫的getValue方法
;
       4: areturn

  public void setValue(java.lang.Object);   //編譯時由編譯器生成的巧方法
    Code:
       0: aload_0
       1: aload_1
       2: checkcast     #26                 // class java/util/Date
       5: invokevirtual #30                 // Method setValue:(Ljava/util/Date;   去調用我們重寫的setValue方法
)V
       8: return
}
從編譯的結果來看,我們本意重寫setValue和getValue方法的子類,竟然有4個方法,其實不用驚奇,最後的兩個方法,就是編譯器自己生成的橋方法。可以看到橋方法的參數類型都是Object,也就是說,子類中真正覆蓋父類兩個方法的就是這兩個我們看不到的橋方法。而打在我們自己定義的setvalue和getValue方法上面的@Oveerride只不過是假象。而橋方法的內部實現,就只是去調用我們自己重寫的那兩個方法。

所以,虛擬機巧妙的使用了巧方法,來解決了類型擦除和多態的衝突。

不過,要提到一點,這裏面的setValue和getValue這兩個橋方法的意義又有不同。

setValue方法是爲了解決類型擦除與多態之間的衝突。

而getValue卻有普遍的意義,怎麼說呢,如果這是一個普通的繼承關係:

那麼父類的setValue方法如下:

public ObjectgetValue() {
		return super.getValue();
	}
而子類重寫的方法是:

public Date getValue() {
		return super.getValue();
	}
其實這在普通的類繼承中也是普遍存在的重寫,這就是協變。

關於協變:。。。。。。

並且,還有一點也許會有疑問,子類中的巧方法  Object   getValue()和Date getValue()是同 時存在的,可是如果是常規的兩個方法,他們的方法簽名是一樣的,也就是說虛擬機根本不能分別這兩個方法。如果是我們自己編寫Java代碼,這樣的代碼是無法通過編譯器的檢查的,但是虛擬機卻是允許這樣做的,因爲虛擬機通過參數類型和返回類型來確定一個方法,所以編譯器爲了實現泛型的多態允許自己做這個看起來“不合法”的事情,然後交給虛擬器去區別。







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