Java 理論和實踐: 瞭解泛型

轉載自 : http://www.ibm.com/developerworks/cn/java/j-jtp01255.htmlhtml

表面上看起來,不管語法仍是應用的環境(好比容器類),泛型類型(或者泛型)都相似於 C++ 中的模板。可是這種類似性僅限於表面,Java 語言中的泛型基本上徹底在編譯器中實現,由編譯器執行類型檢查和類型推斷,而後生成普通的非泛型的字節碼。這種實現技術稱爲 擦除(erasure)(編譯器使用泛型類型信息保證類型安全,而後在生成字節碼以前將其清除),這項技術有一些奇怪,而且有時會帶來一些使人迷惑的後果。雖然範型是 Java 類走向類型安全的一大步,可是在學習使用泛型的過程當中幾乎確定會遇到頭痛(有時候讓人沒法忍受)的問題。java

注意:本文假設您對 JDK 5.0 中的範型有基本的瞭解。數組

泛型不是協變的安全

雖然將集合看做是數組的抽象會有所幫助,可是數組還有一些集合不具有的特殊性質。Java 語言中的數組是協變的(covariant),也就是說,若是 Integer擴展了 Number(事實也是如此),那麼不只 Integer是 Number,並且 Integer[]也是 Number[],在要求Number[]的地方徹底能夠傳遞或者賦予 Integer[]。(更正式地說,若是 Number是 Integer的超類型,那麼 Number[]也是Integer[]的超類型)。您也許認爲這一原理一樣適用於泛型類型 —— List<Number>是 List<Integer>的超類型,那麼能夠在須要List<Number>的地方傳遞 List<Integer>。不幸的是,狀況並不是如此。架構

不容許這樣作有一個很充分的理由:這樣作將破壞要提供的類型安全泛型。若是可以將 List<Integer>賦給 List<Number>。那麼下面的代碼就容許將非 Integer的內容放入 List<Integer>框架

 List<Integer> li = new ArrayList<Integer>(); 
 List<Number> ln = li; // illegal 
 ln.add(new Float(3.1415)); 

 

由於 ln是 List<Number>,因此向其添加 Float彷佛是徹底合法的。可是若是 ln是 li的別名,那麼這就破壞了蘊含在 li定義中的類型安全承諾 —— 它是一個整數列表,這就是泛型類型不能協變的緣由。jsp

其餘的協變問題函數

數組可以協變而泛型不能協變的另外一個後果是,不能實例化泛型類型的數組(new List<String>[3]是不合法的),除非類型參數是一個未綁定的通配符(new List<?>[3]是合法的)。讓咱們看看若是容許聲明泛型類型數組會形成什麼後果:學習

 List<String>[] lsa = new List<String>[10]; // illegal 
 Object[] oa = lsa;  // OK because List<String> is a subtype of Object 
 List<Integer> li = new ArrayList<Integer>(); 
 li.add(new Integer(3)); 
 oa[0] = li; 
 String s = lsa[0].get(0); 

 

最後一行將拋出 ClassCastException,由於這樣將把 List<Integer>填入本應是 List<String>的位置。由於數組協變會破壞泛型的類型安全,因此不容許實例化泛型類型的數組(除非類型參數是未綁定的通配符,好比 List<?>)。this

 

構造延遲

由於能夠擦除功能,因此 List<Integer>和 List<String>是同一個類,編譯器在編譯 List<V>時只生成一個類(和 C++ 不一樣)。所以,在編譯 List<V>類時,編譯器不知道 V所表示的類型,因此它就不能像知道類所表示的具體類型那樣處理 List<V>類定義中的類型參數(List<V>中的 V)。

由於運行時不能區分 List<String>和 List<Integer>(運行時都是 List),用泛型類型參數標識類型的變量的構造就成了問題。運行時缺少類型信息,這給泛型容器類和但願建立保護性副本的泛型類提出了難題。

好比泛型類 Foo

 class Foo<T> { 
  public void doSomething(T param) { ... } 
 } 

 

假設 doSomething()方法但願複製輸入的 param參數,會怎麼樣呢?沒有多少選擇。您可能但願按如下方式實現 doSomething()

 public void doSomething(T param) { 
  T copy = new T(param);  // illegal 
 } 

 

可是您不能使用類型參數訪問構造函數,由於在編譯的時候還不知道要構造什麼類,所以也就不知道使用什麼構造函數。使用泛型不能表達「T必須擁有一個拷貝構造函數(copy constructor)」(甚至一個無參數的構造函數)這類約束,所以不能使用泛型類型參數所表示的類的構造函數。

clone()怎麼樣呢?假設在 Foo的定義中,T擴展了 Cloneable

 class Foo<T extends Cloneable> { 
  public void doSomething(T param) { 
    T copy = (T) param.clone();  // illegal 
  } 
 } 

 

不幸的是,仍然不能調用 param.clone()。爲何呢?由於 clone()在 Object中是保護訪問的,調用 clone()必須經過將 clone()改寫公共訪問的類引用來完成。可是從新聲明 clone()爲 public 並不知道 T,所以克隆也無濟於事。

構造通配符引用

所以,不能複製在編譯時根本不知道是什麼類的類型引用。那麼使用通配符類型怎麼樣?假設要建立類型爲 Set<?>的參數的保護性副本。您知道 Set有一個拷貝構造函數。並且別人可能曾經告訴過您,若是不知道要設置的內容的類型,最好使用 Set<?>代替原始類型的 Set,由於這種方法引發的未檢查類型轉換警告更少。因而,能夠試着這樣寫:

 class Foo { 
  public void doSomething(Set<?> set) { 
    Set<?> copy = new HashSet<?>(set);  // illegal 
  } 
 } 

 

不幸的是,您不能用通配符類型的參數調用泛型構造函數,即便知道存在這樣的構造函數也不行。不過您能夠這樣作:

 class Foo { 
  public void doSomething(Set<?> set) { 
    Set<?> copy = new HashSet<Object>(set);  
  } 
 } 

 

這種構造不那麼直觀,但它是類型安全的,並且能夠像 new HashSet<?>(set)那樣工做。

構造數組

如何實現 ArrayList<V>?假設類 ArrayList管理一個 V數組,您可能但願用 ArrayList<V>的構造函數建立一個 V數組:

 class ArrayList<V> { 
  private V[] backingArray; 
  public ArrayList() { 
    backingArray = new V[DEFAULT_SIZE]; // illegal 
  } 
 } 

 

可是這段代碼不能工做 —— 不能實例化用類型參數表示的類型數組。編譯器不知道 V到底表示什麼類型,所以不能實例化 V數組。

Collections 類經過一種彆扭的方法繞過了這個問題,在 Collections 類編譯時會產生類型未檢查轉換的警告。ArrayList具體實現的構造函數以下:

 class ArrayList<V> { 
  private V[] backingArray; 
  public ArrayList() { 
    backingArray = (V[]) new Object[DEFAULT_SIZE]; 
  } 
 } 

 

爲什麼這些代碼在訪問 backingArray時沒有產生 ArrayStoreException呢?不管如何,都不能將 Object數組賦給 String數組。由於泛型是經過擦除實現的,backingArray的類型實際上就是 Object[],由於 Object代替了 V。這意味着:實際上這個類指望backingArray是一個 Object數組,可是編譯器要進行額外的類型檢查,以確保它包含 V類型的對象。因此這種方法很奏效,可是很是彆扭,所以不值得效仿(甚至連泛型 Collections 框架的做者都這麼說,請參閱 參考資料)。

還有一種方法就是聲明 backingArray爲 Object數組,並在使用它的各個地方強制將它轉化爲 V[]。仍然會看到類型未檢查轉換警告(與上一種方法同樣),可是它使一些未明確的假設更清楚了(好比 backingArray不該逃避 ArrayList的實現)。

其餘方法

最好的辦法是向構造函數傳遞類文字(Foo.class),這樣,該實現就能在運行時知道 T的值。不採用這種方法的緣由在於向後兼容性 —— 新的泛型集合類不能與 Collections 框架之前的版本兼容。

下面的代碼中 ArrayList採用瞭如下方法:

 public class ArrayList<V> implements List<V> { 
  private V[] backingArray; 
  private Class<V> elementType; 
  public ArrayList(Class<V> elementType) { 
    this.elementType = elementType; 
    backingArray = (V[]) Array.newInstance(elementType, DEFAULT_LENGTH); 
  } 
 } 

 

可是等一等!仍然有不妥的地方,調用 Array.newInstance()時會引發未經檢查的類型轉換。爲何呢?一樣是因爲向後兼容性。Array.newInstance()的簽名是:

 public static Object newInstance(Class<?> componentType, int length) 

 

而不是類型安全的:

 public static<T> T[] newInstance(Class<T> componentType, int length) 

 

爲什麼 Array用這種方式進行泛化呢?一樣是爲了保持向後兼容。要建立基本類型的數組,如 int[],可使用適當的包裝器類中的TYPE字段調用 Array.newInstance()(對於 int,能夠傳遞 Integer.TYPE做爲類文字)。用 Class<T>參數而不是 Class<?>泛化Array.newInstance(),對於引用類型有更好的類型安全,可是就不能使用 Array.newInstance()建立基本類型數組的實例了。也許未來會爲引用類型提供新的 newInstance()版本,這樣就二者兼顧了。

在這裏能夠看到一種模式 —— 與泛型有關的不少問題或者折衷並不是來自泛型自己,而是保持和已有代碼兼容的要求帶來的反作用。

 

泛化已有的類

在轉化現有的庫類來使用泛型方面沒有多少技巧,但與日常的狀況相同,向後兼容性不會憑空而來。我已經討論了兩個例子,其中向後兼容性限制了類庫的泛化。

另外一種不一樣的泛化方法可能不存在向後兼容問題,這就是 Collections.toArray(Object[])。傳入 toArray()的數組有兩個目的 —— 若是集合足夠小,那麼能夠將其內容直接放在提供的數組中。不然,利用反射(reflection)建立相同類型的新數組來接受結果。若是從頭開始重寫 Collections 框架,那麼極可能傳遞給 Collections.toArray()的參數不是一個數組,而是一個類文字:

 interface Collection<E> { 
  public T[] toArray(Class<T super E> elementClass); 
 } 

 

由於 Collections 框架做爲良好類設計的例子被普遍效仿,可是它的設計受到向後兼容性約束,因此這些地方值得您注意,不要盲目效仿。

首先,經常被混淆的泛型 Collections API 的一個重要方面是 containsAll()removeAll()和 retainAll()的簽名。您可能認爲remove()和 removeAll()的簽名應該是:

 interface Collection<E> { 
  public boolean remove(E e);  // not really 
  public void removeAll(Collection<? extends E> c);  // not really 
 } 

 

但實際上倒是:

 interface Collection<E> { 
  public boolean remove(Object o);  
  public void removeAll(Collection<?> c); 
 } 

 

爲何呢?答案一樣是由於向後兼容性。x.remove(o)的接口代表「若是 o包含在 x中,則刪除它,不然什麼也不作。」若是 x是一個泛型集合,那麼 o不必定與 x的類型參數兼容。若是 removeAll()被泛化爲只有類型兼容時才能調用(Collection<? extends E>),那麼在泛化以前,合法的代碼序列就會變得不合法,好比:

 // a collection of Integers 
 Collection c = new HashSet(); 
 // a collection of Objects 
 Collection r = new HashSet(); 
 c.removeAll(r); 

 

若是上述片斷用直觀的方法泛化(將 c設爲 Collection<Integer>r設爲 Collection<Object>),若是 removeAll()的簽名要求其參數爲 Collection<? extends E>而不是 no-op,那麼就沒法編譯上面的代碼。泛型類庫的一個主要目標就是不打破或者改變已有代碼的語義,所以,必須用比從頭從新設計泛型所使用類型約束更弱的類型約束來定義 remove()removeAll()retainAll()containsAll()

在泛型以前設計的類可能阻礙了「顯然的」泛型化方法。這種狀況下就要像上例這樣進行折衷,可是若是從頭設計新的泛型類,理解 Java 類庫中的哪些東西是向後兼容的結果頗有意義,這樣能夠避免不適當的模仿。

 

擦除的實現

由於泛型基本上都是在 Java 編譯器中而不是運行庫中實現的,因此在生成字節碼的時候,差很少全部關於泛型類型的類型信息都被「擦掉」了。換句話說,編譯器生成的代碼與您手工編寫的不用泛型、檢查程序的類型安全後進行強制類型轉換所獲得的代碼基本相同。與 C++ 不一樣,List<Integer>和 List<String>是同一個類(雖然是不一樣的類型但都是 List<?>的子類型,與之前的版本相比,在 JDK 5.0 中這是一個更重要的區別)。

擦除意味着一個類不能同時實現 Comparable<String>和 Comparable<Number>,由於事實上二者都在同一個接口中,指定同一個compareTo()方法。聲明 DecimalString類以便與 String與 Number比較彷佛是明智的,但對於 Java 編譯器來講,這至關於對同一個方法進行了兩次聲明:

public class DecimalString implements Comparable<Number>, Comparable<String>
{ ... } // nope 

 

擦除的另外一個後果是,對泛型類型參數是用強制類型轉換或者 instanceof毫無心義。下面的代碼徹底不會改善代碼的類型安全性:

 public <T> T naiveCast(T t, Object o) { return (T) o; } 

 

編譯器僅僅發出一個類型未檢查轉換警告,由於它不知道這種轉換是否安全。naiveCast()方法實際上根本不做任何轉換,T直接被替換爲 Object,與指望的相反,傳入的對象被強制轉換爲 Object

擦除也是形成上述構造問題的緣由,即不能建立泛型類型的對象,由於編譯器不知道要調用什麼構造函數。若是泛型類須要構造用泛型類型參數來指定類型的對象,那麼構造函數應該接受類文字(Foo.class)並將它們保存起來,以便經過反射建立實例。

 

結束語

泛型是 Java 語言走向類型安全的一大步,可是泛型設施的設計和類庫的泛化並不是未通過妥協。擴展虛擬機指令集來支持泛型被認爲是沒法接受的,由於這會爲 Java 廠商升級其 JVM 形成難以逾越的障礙。所以採用了能夠徹底在編譯器中實現的擦除方法。相似地,在泛型 Java 類庫時,保持向後兼容也爲類庫的泛化方式設置了不少限制,產生了一些混亂的、使人沮喪的結構(如Array.newInstance())。這並不是泛型自己的問題,而是與語言的演化與兼容有關。但這些也使得泛型學習和應用起來更讓人迷惑,更加困難。

 

參考資料

相關文章
相關標籤/搜索