淺談Java泛型編程
1 引言在JDK 1.5中,幾個新的特征被引入Java語言。其中之一就是泛型(generics)。泛型(generics,genericity)又稱為“參數(shù)類型化(parameterized type)”或“模板(templates)”,是和繼承(inheritance)不同而互補的一種組件復(fù)用機制。繼承和泛型的不同之處在于——在一個系統(tǒng)中,繼承層次是垂直方向,從抽象到具體,而泛型是水平方向上的。當(dāng)運用繼承,不同的類型將擁有相同的接口,并獲得了多態(tài)性;當(dāng)運用泛型,將擁有許多不同的類型,并得以相同的算法作用在它們身上。因此,一般說來,當(dāng)類型與實現(xiàn)方法無關(guān)時,使用泛型;否則,用繼承。
泛型技術(shù)最直接聯(lián)想到的用途就是建立容器類型。下面是一個沒有使用泛型技術(shù)的例子: List myIntList = new LinkedList();// 1 myIntLikst.add(new Integer(0));// 2 Integer x = (Integer)myIntList.iterator().next();// 3 顯然,程序員知道究竟是什么具體類型被放進了myIntList中。但是,第3行的類型轉(zhuǎn)換(cast)是必不可少的。因為編譯器僅僅能保證iterator返回的是Object類型。要想保證將這個值傳給一個Integer類型變量是安全的,就必須類型轉(zhuǎn)換。除了使代碼顯得有些混亂外,類型轉(zhuǎn)換更帶來了運行時錯誤的可能性。因為程序員難免會犯錯誤。使用了泛型技術(shù),程序員就可以確切地表達他們的意圖,并且把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是一個帶有類型參數(shù)的泛型接口,在這里就是指Integer。現(xiàn)在,我們在第1行里使用Integer作為類型參數(shù),而不是在第3行里做類型轉(zhuǎn)換。這樣,在編譯時刻,編譯器就能夠檢查程序的正確性——無論何時何地,編譯器都將保證myIntList的正確使用。相反地,類型轉(zhuǎn)換僅僅告訴我們——在這里,程序員認(rèn)為這樣做是對的。采用泛型可以增強代碼可讀性和健壯性(robustness)。
2 定義泛型 public interface List<E> { void add(E x); Iterator<E> iterator(); } public interface Interator<E> { E next(); boolean hasNext(); } 這是一段Collection里代碼,一個完整的泛型定義。尖括號里的E就是形式類型參數(shù)(formal type parameters)。在泛型定義中,類型參數(shù)的用法就像一般具體類型那樣。在引言中,我們看到初始化了一個泛型List——List<Integer>。在這里,類型參數(shù)被賦于實際類型參數(shù)(actual type argument)Integer。你可以想象List<Integer>將獲得這樣的代碼: public interface List { void add(Integer x); Iterator< Integer > iterator(); } 和C++中對模板的處理有很大的不同,這里沒有第2份副本。Java采用的是拭去法(erasure)而C++采用的是膨脹法(expansion)。一個泛型定義只被編譯一次,只生成一個文件,就像一般的class和interface一樣。形式類型參數(shù)可以不止1個,如: class Bar < E, D> { …… }
3 通配符 3.1 泛型和子類下面的這段代碼合法么? List<String> ls = new ArrayList<String> ();// 1 List<Object> lo = ls;// 2 假設(shè)這兩行代碼是正確的,那么下面的操作: lo.add(new Object());// 3 String str = ls.get(0);// 4 將導(dǎo)致運行時刻錯誤。通過別名lo存取ls時,我們可以插入任意類型的對象——ls就不再僅僅持有String了。 Java編譯器消除了這種錯誤發(fā)生的可能性。第2行將導(dǎo)致編譯時刻錯誤。一般地說,如果Foo是Bar的子類,G定義為某種泛型,那么G<Foo>不是G<Bar>的子類。
3.2 通配符如果,我們試圖使用泛型的方法編寫一個打印Collection內(nèi)所有元素的函數(shù),要怎么做? 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); } } 現(xiàn)在,我們可以使用任意類型的Collection作為參數(shù)了。注意,在printCollection內(nèi),用Objcet類型訪問c的元素是安全的,因為任何一種具體類型都是Object的子類。但是這樣的操作是錯誤的: List<?> list = new ArrayList<String>(); list.add(…);// compile-time error! 因為list被定義為List<?>,“?”指代了一個未知類型。list.add(…)無法保證插入的對象類型就是list實際包含的類型。唯一的例外就是null——null可以是任意類型的值。但是,通過一個List<?>引用,調(diào)用get()函數(shù)是可以的——即不會修改Collection的函數(shù),就像printCollection里那樣。盡管不能確定具體的類型,但是都是Object的子類。
3.3 受限通配符現(xiàn)在要創(chuàng)建一個簡單的作圖程序。我們定義了接口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() {……} } 很自然地,我們也會設(shè)計這樣一個函數(shù): void drawAll (List<…> shapes) { for (Shape s : shapes) { s.draw(); } } 尖括號里應(yīng)該填寫什么了?顯然,List<Shape>是行不通的,這在3.1里已經(jīng)說明了。List<?>可以,但是不好,因為如果這樣使用: List<Object> list = new ArrayList<Object>();// 1 list.add(new Object());// 2 drawAll(list);// 3 編譯器認(rèn)為沒有問題,但是運行時刻肯定報錯。在drawAll里,我們實際需要的是Shape的子類,但是List<?>無法在編譯時刻保證這一點。這里的解決方案是受限通配符(bounded wildcard)。這樣做: void drawAll(List<? extends Shape> shapes) { .. … } 如果,再像前一個例子的第3行那樣使用的話,編譯器會報錯。因為編譯器要求shapes的每一個元素的實際類型都是Shape的子類。同使用一般通配符一樣,shapes.add(…)是不允許的,因為,編譯器只能保證插入的是Shape的子類對象,而不能肯定與Collection實際包含的類型是匹配的。
4 泛型函數(shù)考慮設(shè)計這樣一個函數(shù)——把一個數(shù)組中的對象依次插入一個Collection中。我們首先這樣嘗試: void addFromArray(Object[] a, Collection<?> c) { for (Object o : a) { c.add(o);// compile-time error! } } 從前面的介紹中,可以明確這樣是不行的。當(dāng)然Collection<Object>同樣是錯誤的。解決這類問題的方法就是使用泛型函數(shù): static <T> void addFromArray(T[] a, Collection<T> c) { for (T o : a) { c.add(o); } } 但是必須注意,當(dāng)我們執(zhí)行addFromArray時,編譯器將根據(jù)參數(shù)的類型檢查是否安全: 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 至于每一種調(diào)用T究竟是匹配了哪種類型。注意:這不是C++。經(jīng)過編譯,foo只生成一段代碼,T就是Object。編譯器只是在恰當(dāng)?shù)牡胤阶隽饲‘?dāng)?shù)念愋娃D(zhuǎn)換。
4.1 泛型函數(shù)和通配符的選擇什么時候應(yīng)當(dāng)使用泛型函數(shù),什么時候應(yīng)當(dāng)使用通配符呢?先看一段來自Collection里的代碼: interface Collection<E> { public boolean containsAll(Collection<?> c); public boolean addAll(Collection<? extends E> c); } 我們也可以用泛型函數(shù)改寫: interface Collection<E> { public <T> boolean containsAll(Collection<T> c); public <T extends E> boolean addAll(Collection<T> c); } 在containsAll和addAll中,類型參數(shù)T僅僅被使用了一次。函數(shù)返回值并不依賴于類型參數(shù)。這就告訴我們,類型參數(shù)是被用于實現(xiàn)多態(tài)的;它的作用僅僅是允許不同的實際類型在不同的場合下可以被使用。如果是這種情況的話,應(yīng)當(dāng)使用通配符。通配符用來實現(xiàn)彈性的子類化——就像這里試圖表達的那樣。泛型函數(shù)允許類型參數(shù)用來表達函數(shù)以及它的返回值和一個或多個類型參數(shù)之間的依賴性。如果,不存在這樣的依賴性的話,泛型函數(shù)就不應(yīng)當(dāng)被使用。泛型函數(shù)和通配符有時是可以一起使用的,如: class Collections { public static <T> void copy(List<T> dest, List<? extends T> src) { … } } 注意兩個參數(shù)之間的類型依賴性。src內(nèi)包含的對象必須滿足is-a T,只有這樣才能夠被安全的插入dest,因為dest包含的對象是T類型的。當(dāng)然這樣也可以的: 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語法。與傳統(tǒng)的for相比,具有代碼清晰,安全的優(yōu)點。 List<Integer> list= new ArrayList<Integer>(); int result = 0; for (Integer i : list) { result += i.intValue(); } 相當(dāng)于: for (Iterator iter = list.iterator(); iter.hasNext();) { result += ((Integer)i.next()).intValue(); } 同樣也可以作用于數(shù)組: Integer[] ia = new Integer[10]; int result = 0; for (Integer i : ia) { result += i.intValue(); }
5.2 通配符和重載,泛型函數(shù)和重載 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在本質(zhì)上是一樣的。可以這樣認(rèn)為,Java編譯器對泛型的處理只是替我們在適當(dāng)?shù)牡胤郊由狭祟愋娃D(zhuǎn)換而已。所以以上3個foo函數(shù)不構(gòu)成重載。類似的代碼在C++中是可行的,因為C++采用的是膨脹法。針對不同的具體類型,生成不同的副本,List<String>和List<Object>是2個不同類型(STL里沒有Object類型,String應(yīng)為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)。正是預(yù)期的輸出。現(xiàn)在,修改一下: void foo(Object o) { System.out.println(“foo(Object o)”); } void <T> foo(T t) { System.out.println(“foo(T t)”); } 不用嘗試任何例子,因為這已經(jīng)無法通過編譯了: name clash: foo(java.lang.Object o) and <T>foo<T> hava the same erasure 拭去法是這樣處理泛型的: l一個參數(shù)化類型擦拭后應(yīng)該除去參數(shù)(List<T> è List) l一個未受限的類型參數(shù)擦拭后成為Object l一個受限的類型參數(shù)擦拭后成為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 數(shù)組 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行就會出現(xiàn)運行時錯誤,因為2—5行的語法都是沒有問題的。泛型數(shù)組只能這樣用: List<?>[] list = new ArrayList<?>[10]; 這解決問題了么?沒有。因為錯誤還是無法避免,除了第5行必須一個顯式的類型轉(zhuǎn)換。 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 新建參數(shù)類型的對象 <T> static void foo(T t) { //….. T tt = new T();// compile-time error } 又是一個和C++模版的不同之處。Java采取的是拭去法!所以,試圖新建一個參數(shù)類型對象的話,應(yīng)當(dāng)這樣: <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泛型技術(shù)之發(fā)展·程序員,2002年第8,9期 [4] 紫云英·漫談面向?qū)ο蟪绦蛟O(shè)計方法·程序員,2002年第3期