1. 程式人生 > >Java 理論和實踐: 瞭解泛型

Java 理論和實踐: 瞭解泛型

表面上看起來,無論語法還是應用的環境(比如容器類),泛型型別(或者泛型)都類似於 C++ 中的模板。但是這種相似性僅限於表面,Java 語言中的泛型基本上完全在編譯器中實現,由編譯器執行型別檢查和型別推斷,然後生成普通的非泛型的位元組碼。這種實現技術稱為 擦除(erasure)(編譯器使用泛型型別資訊保證型別安全,然後在生成位元組碼之前將其清除),這項技術有一些奇怪,並且有時會帶來一些令人迷惑的後果。雖然範型是 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定義中的型別安全承諾 —— 它是一個整數列表,這就是泛型型別不能協變的原因。

其他的協變問題

陣列能夠協變而泛型不能協變的另一個後果是,不能例項化泛型型別的陣列(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<?>)。

構造延遲

因為可以擦除功能,所以 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())。這並非泛型本身的問題,而是與語言的演化與相容有關。但這些也使得泛型學習和應用起來更讓人迷惑,更加困難。