JDK 中增加的泛型類型是 Java 語言中類型安全的一次重要改進但是對於初次使用泛型類型的用戶來說泛型的某些方面看起來可能不容易明白甚至非常奇怪在本月的Java 理論和實踐中Brian Goetz 分析了束縛第一次使用泛型的用戶的常見陷阱您可以通過討論論壇與作者和其他讀者分享您對本文的看法(也可以單擊本文頂端或底端的討論來訪問這個論壇)
表面上看起來無論語法還是應用的環境(比如容器類)泛型類型(或者泛型)都類似於 C++ 中的模板但是這種相似性僅限於表面Java 語言中的泛型基本上完全在編譯器中實現由編譯器執行類型檢查和類型推斷然後生成普通的非泛型的字節碼這種實現技術稱為擦除(erasure)(編譯器使用泛型類型信息保證類型安全然後在生成字節碼之前將其清除)這項技術有一些奇怪並且有時會帶來一些令人迷惑的後果雖然范型是 Java 類走向類型安全的一大步但是在學習使用泛型的過程中幾乎肯定會遇到頭痛(有時候讓人無法忍受)的問題
注意本文假設您對 JDK 中的范型有基本的了解
泛型不是協變的
雖然將集合看作是數組的抽象會有所幫助但是數組還有一些集合不具備的特殊性質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
lnadd(new Float());
因為 ln 是 List<Number>所以向其添加 Float 似乎是完全合法的但是如果 ln 是 li 的別名那麼這就破壞了蘊含在 li 定義中的類型安全承諾 —— 它是一個整數列表這就是泛型類型不能協變的原因
其他的協變問題
數組能夠協變而泛型不能協變的另一個後果是不能實例化泛型類型的數組(new List<String>[] 是不合法的)除非類型參數是一個未綁定的通配符(new List<?>[] 是合法的)讓我們看看如果允許聲明泛型類型數組會造成什麼後果
List<String>[] lsa = new List<String>[]; // illegal
Object[] oa = lsa; // OK because List<String> is a subtype of Object
List<Integer> li = new ArrayList<Integer>();
liadd(new Integer());
oa[] = li;
String s = lsa[]get();
最後一行將拋出 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) { }
}
在這裡可以看到一種模式 —— 與泛型有關的很多問題或者折衷並非來自泛型本身而是保持和已有代碼兼容的要求帶來的副作用
泛化已有的類
在轉化現有的庫類來使用泛型方面沒有多少技巧但與平常的情況相同向後兼容性不會憑空而來我已經討論了兩個例子其中向後兼容性限制了類庫的泛化
另一種不同的泛化方法可能不存在向後兼容問題這就是 CollectionstoArray(Object[])傳入 toArray() 的數組有兩個目的 —— 如果集合足夠小那麼可以將其內容直接放在提供的數組中否則利用反射(reflection)創建相同類型的新數組來接受結果如果從頭開始重寫 Collections 框架那麼很可能傳遞給 CollectionstoArray() 的參數不是一個數組而是一個類文字
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);
}
為什麼呢?答案同樣是因為向後兼容性xremove(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();
cremoveAll(r);
如果上述片段用直觀的方法泛化(將 c 設為 Collection<Integer>r 設為 Collection<Object>)如果 removeAll() 的簽名要求其參數為 Collection<? extends E> 而不是 noop那麼就無法編譯上面的代碼泛型類庫的一個主要目標就是不打破或者改變已有代碼的語義因此必須用比從頭重新設計泛型所使用類型約束更弱的類型約束來定義 remove()removeAll()retainAll() 和 containsAll()
在泛型之前設計的類可能阻礙了顯然的泛型化方法這種情況下就要像上例這樣進行折衷但是如果從頭設計新的泛型類理解 Java 類庫中的哪些東西是向後兼容的結果很有意義這樣可以避免不適當的模仿
擦除的實現
因為泛型基本上都是在 Java 編譯器中而不是運行庫中實現的所以在生成字節碼的時候差不多所有關於泛型類型的類型信息都被擦掉了換句話說編譯器生成的代碼與您手工編寫的不用泛型檢查程序的類型安全後進行強制類型轉換所得到的代碼基本相同與 C++ 不同List<Integer> 和 List<String> 是同一個類(雖然是不同的類型但都是 List<?> 的子類型與以前的版本相比在 JDK 中這是一個更重要的區別)
擦除意味著一個類不能同時實現 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
擦除也是造成上述構造問題的原因即不能創建泛型類型的對象因為編譯器不知道要調用什麼構造函數如果泛型類需要構造用泛型類型參數來指定類型的對象那麼構造函數應該接受類文字(Fooclass)並將它們保存起來以便通過反射創建實例
結束語
泛型是 Java 語言走向類型安全的一大步但是泛型設施的設計和類庫的泛化並非未經過妥協擴展虛擬機指令集來支持泛型被認為是無法接受的因為這會為 Java 廠商升級其 JVM 造成難以逾越的障礙因此采用了可以完全在編譯器中實現的擦除方法類似地在泛型 Java 類庫時保持向後兼容也為類庫的泛化方式設置了很多限制產生了一些混亂的令人沮喪的結構(如 ArraynewInstance())這並非泛型本身的問題而是與語言的演化與兼容有關但這些也使得泛型學習和應用起來更讓人迷惑更加困難
From:http://tw.wingwit.com/Article/program/Java/JSP/201405/30952.html