Java範型淺析

(轉載自:[url]http://blog.csdn.net/andycpp/archive/2007/08/17/1748731.aspx[/url])
寫的很好,保存一下.

    從jdk1.5開始,Java中開始支持範型了。範型是一個很有用的編程工具,給我們帶來了極大的靈活性。在看了《java核心編程》之後,我小有收穫,寫出來與大家分享。
    所謂範型,我的感覺就是,不用考慮對象的具體類型,就可以對對象進行一定的操作,對任何對象都能進行同樣的操作。這就是靈活性之所在。但是,正是因爲沒有 考慮對象的具體類型,因此一般情況下不可以使用對象自帶的接口函數,因爲不同的對象所攜帶的接口函數不一樣,你使用了對象A的接口函數,萬一別人將一個對 象B傳給範型,那麼程序就會出現錯誤,這就是範型的侷限性。所以說,範型的最佳用途,就是用於實現容器類,實現一個通用的容器。該容器可以存儲對象,也可 以取出對象,而不用考慮對象的具體類型。因此,在學習範型的時候,一定要了解這一點,你不能指望範型是萬能的,要充分考慮到範型的侷限性。下面我們來探討 一下範型的原理以及高級應用。首先給出一個範型類:
public class Pair<T> 

   
public Pair() { first = null; second = null; } 
   
public Pair(T first, T second) this.first = first;  this.second = second; } 
 
   
public T getFirst() return first; } 
   
public T getSecond() return second; } 
 
   
public void setFirst(T newValue) { first = newValue; } 
   
public void setSecond(T newValue) { second = newValue; } 
 
   
private T first; 
   
private T second; 
}
 

        我們看到,上述Pair類是一個容器類(我會多次強調,範型天生就是爲了容器類的方便實現),容納了2個數據,但這2個數據類型是不確定的,用範型T來表示。關於範型類如何使用,那是最基本的內容,在此就不討論了。
        下面我們來討論一下Java中範型類的實現原理。在java中,範型是在編譯器中實現的,而不是在虛擬機中實現的,虛擬機對範型一無所知。因此,編譯器一 定要把範型類修改爲普通類,才能夠在虛擬機中執行。在java中,這種技術稱之爲“擦除”,也就是用Object類型替換範型。上述代碼經過擦除後就變成 如下形式:
public class Pair 

   
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; } 
 
   
private Object first; 
   
private Object second; 
}
 

        大家可以看到,這是一個普通類,所有的範型都被替換爲Object類型,他被稱之爲原生類。每當你用一個具體類去實例化該範型時,編譯器都會在原生類的基礎上,通過強制約束在需要的地方添加強制轉換代碼來滿足需求,但是不會生成更多的具體的類(這一點和c++完全不同)。我們來舉例說明這一點:
Pair<Employee>  buddies  =  new  Pair<Employee>(); 

//在上述原生代碼中,此處參數類型是Object,理論上可以接納各種類型,但編譯器通過強制約束
//你只能在此使用Employee(及子類)類型的參數,其他類型編譯器一律報錯
buddies.setFirst(new Employee("張三")); 

//在上述原生代碼中,getFirst()的返回值是一個Object類型,是不可以直接賦給類型爲Employee的buddy的
//但編譯器在此做了手腳,添加了強制轉化代碼,實際代碼應該是Employee buddy = (Employee)buddies.getFirst();
//這樣就合法了。但編譯器做過手腳的代碼你是看不到的,他是以字節碼的形式完成的。
Employee buddy = buddies.getFirst();

        下面我們再來考察一個更復雜的情況,如果我們的Pair類要保證第二個屬性一定要大於第一個屬性,該如何做?這就涉及到兩個屬性的比較,但是這2個屬性類 型未知,可以比較嗎?我們前面也講過,一般情況下不要涉及類型的具體信息。但是現在要比較2個屬性,不得不涉及類型的具體信息了。Java還是考慮到了這 一點,那就是,範型類可以繼承自某一個父類,或者實現某個接口,或者同時繼承父類並且實現接口。這樣的話,就可以對類型調用父類或接口中定義的方法了。代 碼如下:
public class Pair<T extends Comparable> 

   
public boolean setSecond(T newValue) 
   boolean flag 
= false;
   If(newValue.compareTo(first)
>0{
     second 
= newValue;
     flag 
= true;
   }

   
return flag;
}
 
 
   
private T first; 
   
private T second; 
}
 

        我們看到,上面的範型T被我們添加了一個約束條件,那就是他必須實現Comparable接口,這樣的話,我們就可以對範型T使用接口中定義的方法了,也 就可以實現2個元素大小的比較。有人可能要問了,實現一個接口不是用implements嗎?上面怎麼用extends呢??爲了簡化範型的設計,無論是 繼承類還是實現接口,一律使用extends關鍵字。這是規定,沒辦法,記住就行了。若同時添加多個約束,各個約束之間用“&”分隔,比 如:public class Pair<T extends Comparable & Serializable>。那麼編譯器是如何處理這種情況呢?前面講過,範型類最終都會被轉化爲原生類。在前面沒有添加約束的時候,編譯器將範型 通通替換爲Object;而增加了約束之後,通通用第一個約束來替換範型(上面的代碼就會用
Comparable來替換所有範型),當需要用到其他約束中定義的方法的時候,通過插入強制轉化代碼來實現。在此就不給出具體的例子了。
        下面我們來看看最後一個知識點,定義一個函數,該函數接受一個範型類作爲參數。首先讓我們來看一個最簡單的情況,參數是一個實例化的範型類:
    public static void test(ArrayList<Number> l) {
        l.add(
new Integer(2));
    }

        上述代碼中,形參list的元素被實例化爲Number類型。在使用該函數的時候我們能不能傳入一個元素爲Integer的list呢?看看下面代碼合法嗎?
    ArrayList<Integer> l = new ArrayList<Integer>();
    test(l);  
//此處編譯器會報錯!!

        答案上面已經給出了:不行!對於這種形參,實參的類型必須和他完全一致,即也應該是一個元素爲Number的list纔可以,其他的實參一律不行。這是爲 什麼呢?Integer不是Number的子類嗎?子類的對象傳遞給父類的引用,不可以嗎?這裏我們就要注意了,Integer確實是Number的子類,但是,ArrayList<Integer>並不是ArrayList<Number>的子類,二者之間沒有任何的繼承關係!!因此這樣傳遞參數是不允許的。如果允許的話,會出現什麼問題嗎?當然會,我們對test函數重新定義一下:
    public static void test(ArrayList<Number> l) {
        l.add(
new Float(2));
    }

        大家可以看到,在函數內部,我們把Float類型的元素插入到鏈表中。因爲鏈表是Number類型,這條語句沒問題。但是,如果實參是一個Integer 類型的鏈表,他能存儲Float類型的數據嗎??顯然不能,這樣就會造成運行時錯誤。於是,編譯器乾脆就不允許進行這樣的傳遞。
        通過分析我們看到,出錯的可能性只有一個:在向容器類添加內 容的時候可能造成類型不匹配。那麼有些人可能會有這種要求:“我保證一定不對容器添加內容,我非常希望能夠將一個Integer類(Number類的子 類)組成的鏈表傳遞進來”。Sun的那幫大牛們當然會考慮到這種訴求,這樣的功能是可以實現的,並且還有兩種方式呢,看下面代碼:
//     1.在定義方法的時候使用Wildcard(也就是下述代碼中的問號)。
    public static void test1(ArrayList<? extends Number> l) {
        Integer n 
= new Integer(45);
        Number x 
= l.get(0); //從鏈表中取數據是允許的
        l.add(n);  //錯誤!!往鏈表裏面插入數據是被編譯器嚴格禁止的!!
    }


//     2.定義一個範型方法。代碼如下:
    public static <extends Number> void test2(ArrayList<T> l) {
        Number n 
= l.get(0);
        T d 
= l.get(0);
        l.add(d);  
//與上面的方法相比,插入一個範型數據是被允許的,相對靈活一些
        l.add(n);  //錯誤!!只可以插入範型數據,絕不可插入具體類型數據。
    }

        按照上述代碼的寫法,只要我們對形參添加了一定的約束條件,那麼我們在傳遞實參的時候,對實參的嚴格約束就會降低一些。上述代碼都指定了一個類 Number,並用了extends關鍵字,因此,在傳遞實參的時候,凡是從Number繼承的類組成的鏈表,均可以傳遞進去。但上面代碼的註釋中也說的 很清楚,爲了不出現運行時錯誤,編譯器會對你調用的方法做嚴格的限制:凡是參數爲範型的方法,一律不需調用!!
l.get(0)是合法的,因爲參數是整型而不是範型;l.add(x)就不合法,因爲add函數的參數是範型。但是定義一個範型方法還是有一定靈活性的,如果傳入的數據也是範型,編譯器還是認可的,因爲範型對範型,類型安全是可以保證的。
        從上述代碼可以看出,定義一個範型方法要比Wildcard稍微靈活一些,可以往鏈表中添加T類型的對象,而Wildcard中是不允許往鏈表中添加任何類型的對象的。那麼我們還要Wildcard幹什麼呢?
Wildcard還是有他存在的意義的,那就是,Wildcard支持另外一個關鍵字super,而範型方法不支持super關鍵字。換句話說,如果你要實現這樣的功能:“傳入的參數應該是指定類的父類”,範型方法就無能爲力了,只能依靠Wildcard來實現。代碼如下:
    public static void test5(ArrayList<? super Integer> l) {
        Integer n 
= new Integer(45);
        l.add(n);  
//與上面使用extends關鍵字相反,往鏈表裏面插入指定類型的數據是被允許的。
        Object x = l.get(0); //從鏈表裏取出一個數據仍然是被允許的,不過要賦值給Object對象。
        l.add(x);   //錯誤!!將剛剛取出的數據再次插入鏈表是不被允許的。
    }
        這種實現方式的特點我們前面已經說過了,就是對實參的限制更改爲:必須是指定類型的父類。這裏我們指定了Integer類,那麼實參鏈表的元素類型,必須 是Number類及其父類。下面我們重點討論一下上述代碼的第四條語句,爲什麼將剛剛取出的數據再次插入鏈表不被允許??道理很簡單,剛剛取出的數據被保 存在一個Object類型的引用中,而鏈表的add方法只能接受指定類型Integer及其子類,類型不匹配當然不行。有些人可能立刻會說,我將他強制轉 化爲Integer類(即
l.add((Integer)x); , 編譯器不就不報錯了嗎?確實,經過強制轉化後,編譯器確實沒意見了。不過這種強制轉化有可能帶來運行時錯誤。因爲你傳入的實參,其元素類型是 Integer的父類,比如是Number。那麼,存儲在該鏈表中的第一個數據,很有可能是Double或其他類型的,這是合法的。那麼你取出的第一個元 素x也會是Double類型。那麼你把一個Double類型強制轉化爲Integer類型,顯然是一個運行時錯誤。
        難道“把取出的元素再插入到鏈表中”這樣一個功能就實現不了嗎?當然可以,不過不能直接實現,要藉助範型函數的幫忙,因爲在範型函數中,剛剛取出的元素再存回去是不成問題的。定義這樣一個範型函數,我們稱之爲幫助函數。代碼如下:
    //幫助函數
    public static <T>void helperTest5(ArrayList<T> l, int index) {
        T temp 
= l.get(index);
        l.add(temp);
    }

    
    
//主功能函數
    public static void test5(ArrayList<? super Integer> l) {
        Integer n 
= new Integer(45);
        l.add(n);  
        helperTest5(l, 
0);   //通過幫助類,將指定的元素取出後再插回去。
    }

        上述兩個函數結合的原理就是:利用Wildcard的super關鍵字來限制參數的類型(範型函數不支持super,要是支持的話就不用這麼麻煩了),然後通過範型函數來完成取出數據的再存儲。
        以上就是我學習範型的所有心得。下面再把《Java核心編程》中列出的使用範型時的注意事項列出來(各種操作被禁止的原因就不具體說明了),供大家參考:
//1、不可以用一個本地類型(如int   float)來替換範型
//2、運行時類型檢查,不同類型的範型類是等價的(Pair<String>與Pair<Employee>是屬於同一個類型Pair),
//     這一點要特別注意,即如果a instanceof Pair<String>==true的話,並不代表a.getFirst()的返回值是一個String類型
//3、範型類不可以繼承Exception類,即範型類不可以作爲異常被拋出
//4、不可以定義範型數組
//5、不可以用範型構造對象,即first = new T(); 是錯誤的
//6、在static方法中不可以使用範型,範型變量也不可以用static關鍵字來修飾
//7、不要在範型類中定義equals(T x)這類方法,因爲Object類中也有equals方法,當範型類被擦除後,這兩個方法會衝突
//8、根據同一個範型類衍生出來的多個類之間沒有任何關係,不可以互相賦值
//     即Pair<Number> p1;  Pair<Integer> p2;   p1=p2;  這種賦值是錯誤的。
//9、若某個範型類還有同名的非範型類,不要混合使用,堅持使用範型類
//     Pair<Manager> managerBuddies = new Pair<Manager>(ceo, cfo);
//     Pair rawBuddies = managerBuddies;  這裏編譯器不會報錯,但存在着嚴重的運行時錯誤隱患
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章