程式扎記: [ Gossip in Java ] 泛型(Generics)介紹

標籤

2010年12月13日 星期一

[ Gossip in Java ] 泛型(Generics)介紹

前言 
J2SE 5.0 提供的泛型,目的在讓您定義「安全的」泛型類別(Generics class),事實上 J2SE 5.0 前就用 Object 解決了泛型類別的部份需求,J2SE 5.0 之後再解決的是型態安全問題. 

沒有泛型之前 : 
考慮您要設計下面的兩個類別(兩個很無聊的類別,但足以說明需求): 
- BooleanFoo.java 
  1. public class BooleanFoo {    
  2.     private Boolean foo;    
  3.     
  4.     public void setFoo(Boolean foo) {    
  5.         this.foo = foo;    
  6.     }    
  7.     
  8.     public Boolean getFoo() {    
  9.         return foo;    
  10.     }    
  11. }    
- IntegerFoo.java 
  1. public class IntegerFoo {    
  2.     private Integer foo;    
  3.     
  4.     public void setFoo(Integer foo) {    
  5.         this.foo = foo;    
  6.     }    
  7.     
  8.     public Integer getFoo() {    
  9.         return foo;    
  10.     }    
  11. }   
類別定義時邏輯完全一樣,只是當中宣告的成員型態不同,有點小聰明的程式設計人員會將第一個類的內容複製至另一個檔案中,然後用編輯器「取代」功能一次取 代所有的型態名稱(即將 Boolean 取代為 Integer). OK!是有些小聰明,但還是不太聰明,如果類別中的邏輯要修改,您要修改兩個檔案,泛型(Generics)的需求就在此產生,當您定義類 別時,發現到好幾個類別的邏輯其實都相同,就只是當中所涉及的型態不一樣時,使用複製、貼上、取代的功能來撰寫程式只是讓您增加不必要的檔案管理困擾,有 沒有辦法只寫一個檔案就好,畢竟它們的邏輯是相同的. 別忘了,Java 中所有的類別都擴充自Object,這樣寫會比較好 : 
- ObjectFoo.java 代碼 : 
  1. public class ObjectFoo {    
  2.     private Object foo;    
  3.     
  4.     public void setFoo(Object foo) {    
  5.         this.foo = foo;    
  6.     }    
  7.     
  8.     public Object getFoo() {    
  9.         return foo;    
  10.     }    
  11. }   
由於 Java 中所有定義的類別,都以 Object 為最上層的父類別,所以用它來實現泛型(Generics)功能是一個不錯的考量,大部份的人都這麼作,您只要撰寫這麼一個類別,然後可以如下的使用它 : 
  1. ObjectFoo foo1 = new ObjectFoo();    
  2. ObjectFoo foo2 = new ObjectFoo();    
  3.     
  4. foo1.setFoo(new Boolean(true));    
  5. // 記得轉換介面    
  6. Boolean b = (Boolean) foo1.getFoo();    
  7.     
  8. // 記得轉換介面    
  9. foo2.setFoo(new Integer(10));    
  10. Integer i = (Integer) foo2.getFoo();    
看來還不錯,但是由於傳回的是 Object,您必須轉換它的介面,問題出在這邊,粗心的程式設計人員往往會忘了要作這個動作,或者是轉換介面時用錯了型態(像是該用 Boolean 卻用了 Integer[/color]). 要命的是,語法上是可以的,所以編譯器檢查不出錯誤,真正的錯誤要在執行時期才會發生,這時惱人的 ClassCastException 就會出來搞怪,在使用 Object 設計泛型程式時,程式人員要再細心一些、小心一些. 

定義泛型類別 : 
當您定義類別時,發現到好幾個類別的邏輯其實都相同,就只是當中所涉及的型態不一樣時,使用複製、貼上、取代的功能來撰寫程式只是讓您增加不必要的檔案管理困擾. 由於Java中所有定義的類別,都以 Object 為最上層的父類別,所以在 J2SE 5.0 之前,Java程式設計人員可以使用 Object 來解決上面這樣的需求,為了讓定義出來的類別可以更加通用(Generic),傳入的值或傳回的物件都是以 Object 為主,當您要取出這些物件來使用時,必須記得將介面轉換為原來的類型,這樣才可以操作物件上的方法. 
在 J2SE 5.0 之後,提出了針對泛型(Generics)設計的解決方案,要定義一個簡單的泛型類別是簡單的,直接來看個例子 : 
- GenericFoo.java 代碼 : 
  1. public class GenericFoo<T> {    
  2.     private T foo;    
  3.     
  4.     public void setFoo(T foo) {    
  5.         this.foo = foo;    
  6.     }    
  7.     
  8.     public T getFoo() {    
  9.         return foo;    
  10.     }    
  11. }    
用來宣告一個型態持有者(HolderT,之後您可以用 T 作為型態代表來宣告變數(參考)名稱,然後您可以像下面的程式來使用這個類別 : 
  1. GenericFoo<Boolean> foo1 = new GenericFoo<Boolean>();    
  2. GenericFoo<Integer> foo2 = new GenericFoo<Integer>();    
  3.     
  4. foo1.setFoo(new Boolean(true));    
  5. Boolean b = foo1.getFoo();    
  6.     
  7. foo2.setFoo(new Integer(10));    
  8. Integer i = foo2.getFoo();    
不同的地方在於,在宣告與配置物件時,您可以一併指定泛型類別中真正的型態,這將用來取代定義時所使用的 T,而這次您可以看到,介面轉換不再需要了,所定義出來的泛型類別在使用時多了一層安全性,至少可以省去惱人的 ClassCastException 發生,編譯器可以幫您作第一層防線,例如下面的程式會被檢查出錯誤 : 
  1. GenericFoo<Boolean> foo1 = new GenericFoo<Boolean>();    
  2. foo1.setFoo(new Boolean(true));    
  3. Integer b = foo1.getFoo(); //編譯時發生錯誤  
如果使用泛型類別,但宣告時不一併指定型態時預設會使用 Object,不過您就要自己轉換物件的介面型態了,但編譯器會提出警訊,告訴您這可能是不安全的操作 : 
Note: Test.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

回過頭來看看下面的宣告 : 
  1. GenericFoo<Boolean> foo1 = new GenericFoo<Boolean>();  
  2. GenericFoo<Integer> foo2 = new GenericFoo<Integer>();  
GenericFoo<Boolean> 宣告的 foo1 與 GenericFoo<Integer> 宣告的 foo2 是相同的類型嗎?答案是否定的,基本上它們分屬於兩個不同類別的類型, 所以您不可以將 foo1 指定給 foo2,或是將 foo2 指定給 foo1,編譯器會錯誤. 

幾個定義泛型的例子 : 
您可以在定義泛型類別時,宣告多個類型持有者,例如 : 
  1. package gossip.generic;  
  2.   
  3. public class GenericFoo<T1,T2> {  
  4.     private T1 foo1;    
  5.     private T2 foo2;    
  6.     
  7.     public void setFoo1(T1 foo1) {    
  8.         this.foo1 = foo1;    
  9.     }    
  10.     
  11.     public T1 getFoo1() {    
  12.         return foo1;    
  13.     }    
  14.     
  15.     public void setFoo2(T2 foo2) {    
  16.         this.foo2 = foo2;    
  17.     }    
  18.     
  19.     public T2 getFoo2() {    
  20.         return foo2;    
  21.     }    
  22. }  
您可以如下使用 GenericFoo 類別,分別以 Integer 與 Boolean 取代 T1 與 T2 : 
  1. GenericFoo<Integer,Boolean> genericFoo = new GenericFoo<Integer,Boolean>();   
如果您已經定義了一個泛型類別,想要用這個類別來於另一個泛型類別中宣告成員的話要如何作?舉個實例,假設您已經定義了下面的類別 : 
  1. public class GenericFoo<T> {  
  2.     private T foo;    
  3.         
  4.     public void setFoo(T foo) {    
  5.         this.foo = foo;    
  6.     }    
  7.     
  8.     public T getFoo() {    
  9.         return foo;    
  10.     }   
  11. }  
您想要寫一個包裝類別(Wrapper),這個類別必須也具有 GenericFoo 的泛型功能,您可以這麼寫 : 
  1. package gossip.generic;  
  2.   
  3. public class WrapperFoo<T> {  
  4.     private GenericFoo<T> foo;    
  5.       
  6.     public void setFoo(GenericFoo<T> foo) {    
  7.         this.foo = foo;    
  8.     }    
  9.     
  10.     public GenericFoo<T> getFoo() {    
  11.         return foo;    
  12.     }    
  13. }  
這麼一來,您就可以保留型態持有者 T 的功能,一個使用的例子如下 : 
  1. GenericFoo<Integer> foo = new GenericFoo<Integer>();    
  2. foo.setFoo(new Integer(10));    
  3.     
  4. WrapperFoo<Integer> wrapper = new WrapperFoo<Integer>();    
  5. wrapper.setFoo(foo);  
限制泛型可用類型 : 
在定義泛型類別時,預設您可以使用任何的型態來實例化泛型類別中的型態持有者,但假設您想要限制使用泛型類別時,只能用某個特定型態或其子類別才能實例化型態持有者的話, 您可以在定義型態持有者時,一併使用 "extends" 指定這個型態持有者必須是擴充某個類型,舉個實例來說 : 
- ListGenericFoo.java 代碼 : 
  1. package gossip.generic;  
  2.   
  3. import java.util.List;  
  4.   
  5. public class ListGenericFoo<T extends List> {  
  6.     private T[] fooArray;    
  7.         
  8.     public void setFooArray(T[] fooArray) {    
  9.         this.fooArray = fooArray;    
  10.     }    
  11.     
  12.     public T[] getFooArray() {    
  13.         return fooArray;    
  14.     }    
  15. }  
ListGenericFoo 在宣告類型持有者時,一併指定這個持有者必須擴充自 List 介面(interface),在限定持有者時,無論是要限定的對 象是介面或類別,都是使用 "extends" 關鍵字. 您使用 "extends" 限定型態持有者必須是實作 List 的類別或其子類別,例如LinkedList 與 ArrayList,下面的程式是合法的 : 
  1. ListGenericFoo<LinkedList> foo1 = new ListGenericFoo<LinkedList>();  
  2. ListGenericFoo<ArrayList> foo2 = new ListGenericFoo<ArrayList>();  
但是如果不是 List 的類別或是其子類別,就會發生編譯錯誤,例如下面的程式通不過編譯 : 
  1. ListGenericFoo<HashMap> foo3 = new ListGenericFoo<HashMap>();   
型態通配字元 : 
假設您撰寫了一個泛型類別 : 
- GenericFoo.java 代碼 : 
  1. public class GenericFoo<T> {  
  2.     private T foo;    
  3.         
  4.     public void setFoo(T foo) {    
  5.         this.foo = foo;    
  6.     }    
  7.     
  8.     public T getFoo() {    
  9.         return foo;    
  10.     }   
  11. }  
分別使用下面的程式宣告了 foo1 與 foo2 兩個參考名稱 : 
  1. GenericFoo<ArrayList> foo1 = null;  
  2. GenericFoo<LinkedList> foo2 = null;  
那麼 foo1 就只接受 GenericFoo<ArrayList> 的實例,而 foo2 只接受 GenericFoo<LinkedList> 的實例. 現在您有這麼一個需求,您希望有一個參考名稱 foo 可以接受所有下面的實例(ListMap 或 List 介面以及其實介面的相關類別,在 J2SE 5.0中已經針對泛型功能作了改寫,在這邊仍請將之當作介面就好,這是為了簡化說明的考量): 
  1. foo = new GenericFoo<List>();  
  2. foo = new GenericFoo<Map>();  
簡單的說,實例化型態持有者時,它必須是實作 List 的類別或其子類別,要宣告這麼一個參考名稱,您可以使用 '?' 通配字元,並使用 "extends" 關鍵字限定型態持有者的型態,例如 : 
  1. GenericFoo<? extends List> foo = null;  
  2. foo = new GenericFoo<LinkedList>();  
  3. foo = new GenericFoo<ArrayList>();  
如果指定了不是實作 List 的類別或其子類別,則編譯器會回報錯誤. 這樣的限定是很有用的,例如如果您想要自訂一個 showFoo() 方法,方法的內容實作是針對 List 而制定的,例如 : 
  1. public void showFoo(GenericFoo foo) {    
  2.      // 針對List而制定的內容    
  3. }   
您當然不希望任何的型態都可以傳入 showFoo() 方法中,您可以使用以下的方式來限定,例如 : 
  1. public void showFoo(GenericFoo<? extends List> foo) {    
  2.      //     
  3. }  
這麼一來,如果有粗心的程式設計人員傳入了您不想要的型態,例如 GenericFoo<Map> 型態的實例,則編譯器都會告訴它這是不可行的,在宣告名稱時如果指定了 "?" 而 不使用 "extends",則預設是允許 Object 及其下的子類,也就是所有的 Java 物件了,那為什麼不直接使用 GenericFoo<Object> 宣告就好了,何必要用GenericFoo<?> 來宣告?使用通配字元有點要注意的是,透過使用通配字元宣告的名稱所參考的物件,您沒辦法再對它加入新的資訊,您只能取得它的資訊或是移除它的資訊,例如 : 
  1. GenericFoo<String> foo = new GenericFoo<String>();    
  2. foo.setFoo("caterpillar");    
  3. GenericFoo<?> immutableFoo = foo;    
  4.     
  5. // 可以取得資訊    
  6. System.out.println(immutableFoo.getFoo());    
  7.     
  8. // 可透過immutableFoo來移去foo所參考實例內的資訊    
  9. immutableFoo.setFoo(null);    
  10.     
  11. // 不可透過immutableFoo來設定新的資訊給foo所參考的實例    
  12. // 所以下面這行無法通過編譯    
  13. //immutableFoo.setFoo("John");   
所以使用或是的宣告方式,意味著您只能透過該名稱來取得所參考實例的資訊,或者是移除某些資訊,但不能增加它的資訊,因為只知道當中放置的 是SomeClass的子類,但不確定是什麼類的實例,編譯器不讓您加入物件,理由是,如果可以加入物件的話,那麼您就得記得取回的物件實例是什麼形態, 然後轉換為原來的型態方可進行操作,這就失去了使用泛型的意義. 

事實上,GenericFoo<?> immutableFoo 相當於 GenericFoo immutableFoo. 除了可以向下限制,您也可以向上限制,只要使用 "super" 關鍵字,例如 : 
  1. GenericFoo<? super StringBuilder> foo;  
如此,foo 就只接受 StringBuilder 及其上層的父類型態之物件. 

擴充泛型類別、實作泛型介面 : 
您可以擴充一個泛型類別,保留其型態持有者,並新增自己的型態持有者,例如先寫一個父類別 : 
- GenericFoo.java 
  1. public class GenericFoo<T1, T2> {  
  2.     private T1 foo1;  
  3.     private T2 foo2;  
  4.   
  5.     public void setFoo1(T1 foo1) {  
  6.         this.foo1 = foo1;  
  7.     }  
  8.   
  9.     public T1 getFoo1() {  
  10.         return foo1;  
  11.     }  
  12.   
  13.     public void setFoo2(T2 foo2) {  
  14.         this.foo2 = foo2;  
  15.     }  
  16.   
  17.     public T2 getFoo2() {  
  18.         return foo2;  
  19.     }  
  20. }  
再來寫一個子類別擴充上面的父類別: 
- SubGenericFoo.java 
  1. public class SubGenericFoo<T1, T2, T3>   
  2.                                extends GenericFoo<T1, T2> {  
  3.     private T3 foo3;  
  4.     
  5.     public void setFoo3(T3 foo3) {  
  6.         this.foo3 = foo3;  
  7.     }  
  8.   
  9.     public T3 getFoo3() {  
  10.         return foo3;  
  11.     }  
  12. }  
如果決定要保留型態持有者,則父類別上宣告的型態持有者數目必須齊全,也就是說上式中,T1 與 T2 都要出現,如果不保留型態持有者,則繼承下來的 T1 與 T2 自動變為Object,建議當然是父類別的型態持有者都保留。 

介面實作也是類似,例如先定義一個介面: 
- IFoo.java 
  1. public interface IFoo<T1, T2> {  
  2.     public void setFoo1(T1 foo1);  
  3.     public void setFoo2(T2 foo2);  
  4.     public T1 getFoo1();  
  5.     public T2 getFoo2();  
  6. }  
實作時如下,保留所有的型態持有者: 
- GenericFoo.java 
  1. public class GenericFoo<T1, T2> implements IFoo<T1, T2> {  
  2.     private T1 foo1;  
  3.     private T2 foo2;  
  4.   
  5.     public void setFoo1(T1 foo1) {  
  6.         this.foo1 = foo1;  
  7.     }  
  8.   
  9.     public T1 getFoo1() {  
  10.         return foo1;  
  11.     }  
  12.   
  13.     public void setFoo2(T2 foo2) {  
  14.         this.foo2 = foo2;  
  15.     }  
  16.   
  17.     public T2 getFoo2() {  
  18.         return foo2;  
  19.     }  
  20. }  
Supplement 
Java Gossip: 型態通配字元 
Java Gossip: 擴充泛型類別、實作泛型介面 
Java 1.5 特性 : Generic Types 
JavaWorld@TW Java論壇- Generics 簡介

5 則留言:

  1. GenericFoo foo1 = new GenericFoo(); 你少了<boolean>,可能是你直接key <和>,但是你應該要轉成& lt ;和 & gt ;

    回覆刪除
    回覆
    1. You are right. Thanks for your feedback!

      刪除
  2. 非常感謝您的解說和無私的分享

    回覆刪除

網誌存檔

關於我自己

我的相片
Where there is a will, there is a way!