淺談Java泛型編程
1 引言在JDK 1.5中,幾個新的特征被引入Java語言。其中之一就是泛型(generics)。泛型(generics,genericity)又稱為“參數類型化(parameterized type)”或“模板(templates)”,是和繼承(inheritance)不同而互補的一種組件復用機制。繼承和泛型的不同之處在于——在一個系統中,繼承層次是垂直方向,從抽象到具體,而泛型是水平方向上的。當運用繼承,不同的類型將擁有相同的接口,并獲得了多態性;當運用泛型,將擁有許多不同的類型,并得以相同的算法作用在它們身上。因此,一般說來,當類型與實現方法無關時,使用泛型;否則,用繼承。
泛型技術最直接聯想到的用途就是建立容器類型。下面是一個沒有使用泛型技術的例子: List myIntList = new LinkedList();// 1 myIntLikst.add(new Integer(0));// 2 Integer x = (Integer)myIntList.iterator().next();// 3 顯然,程序員知道究竟是什么具體類型被放進了myIntList中。但是,第3行的類型轉換(cast)是必不可少的。因為編譯器僅僅能保證iterator返回的是Object類型。要想保證將這個值傳給一個Integer類型變量是安全的,就必須類型轉換。除了使代碼顯得有些混亂外,類型轉換更帶來了運行時錯誤的可能性。因為程序員難免會犯錯誤。使用了泛型技術,程序員就可以確切地表達他們的意圖,并且把myIntList限制為包含一種具體類型。下面就是前一個例子采用了泛型的代碼段: List<Integer> myIntList = new LinkedList<Integer>();// 1 myIntLikst.add(new Integer(0));// 2 Integer x = myIntList.iterator().next();// 3 List<Integer>指出了這不是一個隨意的List,而是一個Integer的List。我們說List是一個帶有類型參數的泛型接口,在這里就是指Integer。現在,我們在第1行里使用Integer作為類型參數,而不是在第3行里做類型轉換。這樣,在編譯時刻,編譯器就能夠檢查程序的正確性——無論何時何地,編譯器都將保證myIntList的正確使用。相反地,類型轉換僅僅告訴我們——在這里,程序員認為這樣做是對的。采用泛型可以增強代碼可讀性和健壯性(robustness)。
2 定義泛型 public interface List<E> { void add(E x); Iterator<E> iterator(); } public interface Interator<E> { E next(); boolean hasNext(); } 這是一段Collection里代碼,一個完整的泛型定義。尖括號里的E就是形式類型參數(formal type parameters)。在泛型定義中,類型參數的用法就像一般具體類型那樣。在引言中,我們看到初始化了一個泛型List——List<Integer>。在這里,類型參數被賦于實際類型參數(actual type argument)Integer。你可以想象List<Integer>將獲得這樣的代碼: public interface List { void add(Integer x); Iterator< Integer > iterator(); } 和C++中對模板的處理有很大的不同,這里沒有第2份副本。Java采用的是拭去法(erasure)而C++采用的是膨脹法(expansion)。一個泛型定義只被編譯一次,只生成一個文件,就像一般的class和interface一樣。形式類型參數可以不止1個,如: class Bar < E, D> { …… }
3 通配符 3.1 泛型和子類下面的這段代碼合法么? List<String> ls = new ArrayList<String> ();// 1 List<Object> lo = ls;// 2 假設這兩行代碼是正確的,那么下面的操作: lo.add(new Object());// 3 String str = ls.get(0);// 4 將導致運行時刻錯誤。通過別名lo存取ls時,我們可以插入任意類型的對象——ls就不再僅僅持有String了。 Java編譯器消除了這種錯誤發生的可能性。第2行將導致編譯時刻錯誤。一般地說,如果Foo是Bar的子類,G定義為某種泛型,那么G<Foo>不是G<Bar>的子類。
3.2 通配符如果,我們試圖使用泛型的方法編寫一個打印Collection內所有元素的函數,要怎么做? void printCollection (Collection<Objcet> c) { for (Objcet obj : c) {// jdk 1.5中新增的語法,見5.1 System.out.println(obj); } } 顯然這樣是不行的,因為通過3.1我們可以知道——Collection<Object>不是任何Collection的父類。那么,所有Collection的父類是什么?Collection<?>——未知類型的Collection(collection of unknown),一個元素可以匹配為任意類型的Collection。“?”被稱作通配類型。上述的代碼,可以改寫成這樣: void printCollection(Collection<?> c) { for (Object obj : c) { System.out.println(obj); } } 現在,我們可以使用任意類型的Collection作為參數了。注意,在printCollection內,用Objcet類型訪問c的元素是安全的,因為任何一種具體類型都是Object的子類。但是這樣的操作是錯誤的: List<?> list = new ArrayList<String>(); list.add(…);// compile-time error! 因為list被定義為List<?>,“?”指代了一個未知類型。list.add(…)無法保證插入的對象類型就是list實際包含的類型。唯一的例外就是null——null可以是任意類型的值。但是,通過一個List<?>引用,調用get()函數是可以的——即不會修改Collection的函數,就像printCollection里那樣。盡管不能確定具體的類型,但是都是Object的子類。
3.3 受限通配符現在要創建一個簡單的作圖程序。我們定義了接口Shape: public abstract class Shape { public abstract void draw(); } 然后定義了2個子類: public class Circle extends Shape { ……. public void draw() { … } } public class Rectangle extends Shape { …… public void draw() {……} } 很自然地,我們也會設計這樣一個函數: void drawAll (List<…> shapes) { for (Shape s : shapes) { s.draw(); } } 尖括號里應該填寫什么了?顯然,List<Shape>是行不通的,這在3.1里已經說明了。List<?>可以,但是不好,因為如果這樣使用: List<Object> list = new ArrayList<Object>();// 1 list.add(new Object());// 2 drawAll(list);// 3 編譯器認為沒有問題,但是運行時刻肯定報錯。在drawAll里,我們實際需要的是Shape的子類,但是List<?>無法在編譯時刻保證這一點。這里的解決方案是受限通配符(bounded wildcard)。這樣做: void drawAll(List<? extends Shape> shapes) { .. … } 如果,再像前一個例子的第3行那樣使用的話,編譯器會報錯。因為編譯器要求shapes的每一個元素的實際類型都是Shape的子類。同使用一般通配符一樣,shapes.add(…)是不允許的,因為,編譯器只能保證插入的是Shape的子類對象,而不能肯定與Collection實際包含的類型是匹配的。
4 泛型函數考慮設計這樣一個函數——把一個數組中的對象依次插入一個Collection中。我們首先這樣嘗試: void addFromArray(Object[] a, Collection<?> c) { for (Object o : a) { c.add(o);// compile-time error! } } 從前面的介紹中,可以明確這樣是不行的。當然Collection<Object>同樣是錯誤的。解決這類問題的方法就是使用泛型函數: static <T> void addFromArray(T[] a, Collection<T> c) { for (T o : a) { c.add(o); } } 但是必須注意,當我們執行addFromArray時,編譯器將根據參數的類型檢查是否安全: addFromArray(new String[10], new ArrayList<String>());// OK! addFromArray(new String[10], new ArrayList<Object>());// OK! addFromArray(new Object[10], new ArrayList<String>());// compile-time error! addFromArray(new String[10], new ArrayList<Integer>());// compile-time error! 第3,4行的錯誤是很容易理解的,無論是把一個Object類型對象插入String的List還是把一個String插入Integer的List都是不安全的。不過,如果這樣的代碼是沒有問題的: <T> void foo(T t1, T t2) { System.out.println(t1.getClass()); System.out.println(t2.getClass()); } foo(new Object(), new String());// 顯示 class java.lang.Objectclass.lang.String foo(new Integer(), new String();// 顯示 class java.lang.Integerclass.lang.String foo(new Object[10], new ArrayList<String>()); // 顯示 class [Ljava.lang.Object;class.util.ArrayList foo(new String[10], new ArrayList<Integer>()); // 顯示 class [Ljava.lang.String;class.util.ArrayList 至于每一種調用T究竟是匹配了哪種類型。注意:這不是C++。經過編譯,foo只生成一段代碼,T就是Object。編譯器只是在恰當的地方做了恰當的類型轉換。
4.1 泛型函數和通配符的選擇什么時候應當使用泛型函數,什么時候應當使用通配符呢?先看一段來自Collection里的代碼: interface Collection<E> { public boolean containsAll(Collection<?> c); public boolean addAll(Collection<? extends E> c); } 我們也可以用泛型函數改寫: interface Collection<E> { public <T> boolean containsAll(Collection<T> c); public <T extends E> boolean addAll(Collection<T> c); } 在containsAll和addAll中,類型參數T僅僅被使用了一次。函數返回值并不依賴于類型參數。這就告訴我們,類型參數是被用于實現多態的;它的作用僅僅是允許不同的實際類型在不同的場合下可以被使用。如果是這種情況的話,應當使用通配符。通配符用來實現彈性的子類化——就像這里試圖表達的那樣。泛型函數允許類型參數用來表達函數以及它的返回值和一個或多個類型參數之間的依賴性。如果,不存在這樣的依賴性的話,泛型函數就不應當被使用。泛型函數和通配符有時是可以一起使用的,如: class Collections { public static <T> void copy(List<T> dest, List<? extends T> src) { … } } 注意兩個參數之間的類型依賴性。src內包含的對象必須滿足is-a T,只有這樣才能夠被安全的插入dest,因為dest包含的對象是T類型的。當然這樣也可以的: public static <T, S extends T> void copy(List<T> dest, List<S> src) { … } 但是推薦第一種用法。因為T同時對dest和src起作用,而S僅僅作用于src,沒有其他的什么依賴于它——這種情況下,用通配符取代S比較好。用通配符更加清晰、明了。
5 其他 5.1 增強型for(Enhanced for,foreach)增強型for也是JDK 1.5新引入的Java語法。與傳統的for相比,具有代碼清晰,安全的優點。 List<Integer> list= new ArrayList<Integer>(); int result = 0; for (Integer i : list) { result += i.intValue(); } 相當于: for (Iterator iter = list.iterator(); iter.hasNext();) { result += ((Integer)i.next()).intValue(); } 同樣也可以作用于數組: Integer[] ia = new Integer[10]; int result = 0; for (Integer i : ia) { result += i.intValue(); }
5.2 通配符和重載,泛型函數和重載 void foo(List<String> ls) { System.out.print(“foo(List<String> ls)”); } void foo(List<Object> lo) { System.out.print(“foo(List<Object> lo)”); } void foo(List<?> l) { System.out.print(“foo(List<?> l)”); }
foo(new ArrayList<String>()); foo(new ArrayList<Object>()); foo(new ArrayList<Integer>()); 編譯并運行這段代碼,我們能看到什么?……編譯錯誤——“hava the same erasure”。注意,Java針對泛型采取的是拭去法,不論是List<String>,List<Object>還是List<?>,編譯生成的都是同一段代碼,而且這段代碼和非泛型的List在本質上是一樣的。可以這樣認為,Java編譯器對泛型的處理只是替我們在適當的地方加上了類型轉換而已。所以以上3個foo函數不構成重載。類似的代碼在C++中是可行的,因為C++采用的是膨脹法。針對不同的具體類型,生成不同的副本,List<String>和List<Object>是2個不同類型(STL里沒有Object類型,String應為std::string),因此foo滿足重載的條件。這種用法稱為“顯式特化”(explicit specialization definition)。
再看一下下面這段代碼: void foo(String s) { System.out.println(“foo(String s)”); } void <T> foo(T t) { System.out.println(“foo(T t)”); } foo(“Test”); foo(new Integer(1)); 編譯并運行這段代碼,我們能看到什么?……編譯錯誤?不是! foo(String s) foo(T t)。正是預期的輸出。現在,修改一下: void foo(Object o) { System.out.println(“foo(Object o)”); } void <T> foo(T t) { System.out.println(“foo(T t)”); } 不用嘗試任何例子,因為這已經無法通過編譯了: name clash: foo(java.lang.Object o) and <T>foo<T> hava the same erasure 拭去法是這樣處理泛型的: l一個參數化類型擦拭后應該除去參數(List<T> è List) l一個未受限的類型參數擦拭后成為Object l一個受限的類型參數擦拭后成為bound的類型
但是需要注意以下的代碼: class Foo<E> { public void test1(List<E> list) { … };// List<E>擦拭后èList public <T> void test2(T t) { … }// T擦拭后èObject } class Bar<E, F> extends Foo<F> { public void test1(List<E> list) { … }// compile-time error public void test2(Object o) { … }// compile-time error } 注意不是是覆蓋(override)……
5.3 數組 List<String>[] list = new ArrayList<String>[10]; 似乎是正確的……編譯時錯誤! List<String>[] list = new ArrayList<String>[10];// 1 Object o = list;// 2 Object[] oa = (Object[])o;// 3 oa[1] = new ArrayList<Integer>();// 4 String s = list[1].get();// 5 如果第1行是正確的話,那么第5行就會出現運行時錯誤,因為2—5行的語法都是沒有問題的。泛型數組只能這樣用: List<?>[] list = new ArrayList<?>[10]; 這解決問題了么?沒有。因為錯誤還是無法避免,除了第5行必須一個顯式的類型轉換。 List<String>[] list = new ArrayList<String>[10];// 1 Object o = list;// 2 Object[] oa = (Object[])o;// 3 oa [1] = new ArrayList<Integer>();// 4 String s = (String)list[1].get();// 5 explicit cast
5.4 新建參數類型的對象 <T> static void foo(T t) { //….. T tt = new T();// compile-time error } 又是一個和C++模版的不同之處。Java采取的是拭去法!所以,試圖新建一個參數類型對象的話,應當這樣: <T> static void foo (T t, Class<T> klass) {// JDK 1.5中,Class類用泛型改寫了 // ….. try { T tt = klass.getInstance(); } catch (…) { } }
參考資料: [1] Generics in the Java Programming Language http://java.sun.com/j2se/1.5/pdf/generics-tutorial.pdf [2] Forthcoming Java Programming Language Features http://java.sun.com/j2se/1.5/pdf/Tiger-lang.pdf [3] 侯捷·Java泛型技術之發展·程序員,2002年第8,9期 [4] 紫云英·漫談面向對象程序設計方法·程序員,2002年第3期
posted on 2007-10-31 20:06
末日風情 閱讀(2492)
評論(0) 編輯 收藏 所屬分類:
java編程