自Java 1.5開始使用的泛型,泛型給人比較直觀的印象是..."尖括號里寫了類型我就不用檢查類型也不用強轉了"。
確實,那先從API的使用者的角度上想問題,泛型還有什么意義?
Discover errors as soon as possible after they are made, ideally at compile time.
泛型提供的正是這種能力。比如有一個只允許加入String的集合,在沒有聲明類型參數的情況下,這種限制通常通過注視來保證。接著在集合中add一個Integer實例,從集合獲取元素時使用強轉,結果導致ClassCastException。這樣的錯誤發生在運行時,compiler就愛莫能助了,而聲明了類型參數的情況下則是compile-time error。相比raw type,泛型的優勢顯而易見,即安全性和表述性。
那么,有了泛型就一定比raw type強嗎?如果類型參數是Object又如何? 這種用法和raw type有什么區別? 如果僅僅通過代碼描述,可以說raw type不一定支持哪個類型,而Collection<object>支持任何類型。正確,但沒什么意義。泛型有種規則叫subtyping rules,比如List是List的子類型,但不是List<object>的子類型。下面的代碼描述了這種情況:
// Uses raw type (List) - fails at runtime!public static void main(String[] args) { List<String> strings = new ArrayList<String>(); unsafeAdd(strings, new Integer(42)); String s = strings.get(0); // Compiler-generated cast}PRivate static void unsafeAdd(List list, Object o) { list.add(o);}上面的情況導致運行時才能發現錯誤,而下面這種做法則是編譯無法通過:
Test.java:5: unsafeAdd(List<Object>,Object) cannot be applied to (List<String>,Integer) unsafeAdd(strings, new Integer(42)); ^而為了應對這種情況Java提供了unbounded wildcard type。即,對于不確定的類型參數使用‘?’代替。比如Set<?>可以理解為某個類型的集合。
編碼時幾乎不會用到raw type,但也有兩個例外,而且都和泛型擦除有關。
既然如此,為什么Java還保留著raw type用法?Java 1.5發布的時候Java馬上要迎來第一個十年,早已存在大量的代碼。老代碼和使用的新特性的代碼能夠互用至關重要,即 migration compatibility 。
好了,接下來說說使用泛型方面的事情。關于raw type,在某些IDE中使用了raw type就會出現警告,并提示加上@SuppressWarnings。比如,eclipse中:
@SuppressWarnings到底有什么作用? 作者給我們的提醒是:要盡量消除這些警告。提示我們加上@SuppressWarnings是在傳達一種信息:無法在運行時檢查類型轉換的安全性。而程序員消除這些警告是在傳達一種信息:運行時不會出現ClassCastException。
消除警告時優先使用聲明類型參數的方式,如果因為某些原因而無法消除警告并且需要證明代碼是沒有問題時才使用@SuppressWarnings。如上圖所示的那樣,@SuppressWarnings可以可以用在變量和方法上,對此我們優先針對更小的粒度。對于@SuppressWarnings,不忽略,且不盲目。
接著說說subtyping,總覺得因為這一特征,泛型有時反而顯得麻煩。書中原話是covariant(協變)和invaritant。比如,數組是covariant的,比如Sub是Super的子類,則Sub[]是Super[]的子類。反之,泛型是invariant的,List不是List的子類。
鑒于這種區別,數組和泛型的難以混合使用。比如下面這幾種寫法都是非法的:
new List<E>[]new List<String>[]new E[]下面通過一段代碼說明泛型數組為什么非法:
// Why generic array creation is illegal - won't compile!List<String>[] stringLists = new List<String>[1]; // (1)List<Integer> intList = Arrays.asList(42); // (2)Object[] objects = stringLists; // (3)objects[0] = intList; // (4)String s = stringLists[0].get(0); // (5)首先假設第一行是合法的。第二行本身合法,鑒于第一行是合法并且數組是協變的,第三行也是合法的。鑒于泛型用擦除實現的,即List的運行時類型是List,相應地,List[]的運行時類型是List[],第四行是合法的。到了第五行則變得矛盾,說好的String類型呢?為什么聲明了類型參數還是來了個ClassCastException?既然如此,索性讓第一行產生compile-time error吧,泛型又變得美好了。
舉一個例子,比如下面這段代碼:
interface Function<T> { T apply(T arg1, T arg2);}static Object reduce(List list, Function f, Object initVal) { Object[] snapshot; snapshot = list.toArray(); Object result = initVal; for (Object e : snapshot) result = f.apply(result, e); return result;}現在我想把reduce改為泛型方法,于是改成了如下形式:
static <E> E reduce(List<E> list, Function<E> f, E initVal) { E[] snapshot = (E[])list.toArray(); E result = initVal; for (E e : snapshot) result = f.apply(result, e); return result;}顯然,結果是編譯無法通過。結果是除了在list.toArray上提示加上@SuppressWarnings之外沒有任何問題,完全可以正常運行。不要忽略@SuppressWarnings! 提示我加上@SuppressWarnings是在告訴我:無法在運行時檢查類型轉換的安全性。那我應該加上@SuppressWarnings嗎?如果加上的話又怎么保證?
其實解決方法很簡單,就是不混用數組和泛型,即:
static <E> E reduce(List<E> list, Function<E> f, E initVal) { List<E> snapshot; synchronized (list) { snapshot = new ArrayList<E>(list); } E result = initVal; for (E e : snapshot) result = f.apply(result, e); return result;}這就好了,能用泛型就用。但換個立場,作為一個提供者而不是使用者,問題還會這樣容易嗎? 比如描述一個棧:
// 無法通過編譯public class Stack <E> { private E[] elements; private int size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16; public Stack() { elements = new E[DEFAULT_INITIAL_CAPACITY]; } public void push(E e) { ensureCapacity(); elements[size++] = e; } public E pop() { if (size==0) throw new EmptyStackException(); E result = elements[--size]; elements[size] = null; // Eliminate obsolete reference return result; } // 檢查方法略}很顯然,數組是具體化的,new E[DEFAULTINITIALCAPACITY]上提示Cannot create a generic array of E。于是我機制地改為(E[])new Object[DEFAULTINITIALCAPACITY]。這樣完全可以通過,然后出現提示讓我加上@SuppressWarnings...我無法忽略,但我可以證明不會出現ClassCastException。即elements是private的,且沒有任何方法可以直接訪問它,push和pop的類型都是安全的。或者我也能將elements的類型改為Object[],并且在pop的時候將元素類型轉為E,這樣也是可以的。
與其讓使用者進行強轉,倒不如提供者提供一個安全的泛型。但不是所有的情況都像上面的例子那樣的順利。比如我在Stack中增加了一個:
public void pushAll(Iterable<E> src) { for (E e : src) push(e);}然后我將Iterable傳入Stack中,由于泛型不是協變的,果斷來了個compile-time error。但拋開這些不想,將一堆Integer放到一堆Number又顯得那么里所應當。于是我們就有了bounded wildcard type,關鍵就是這個bounded。一個'?'是wildcard,為其加點限制就是bounded wildcard,即:
public void pushAll(Iterable<? extends E> src) { for (E e : src) push(e);}相應地,我們再提供一個popAll方法,將pop出來的元素添加到指定集合中。比如,Stack中的元素必然可以添加到Collection<object>中,也就是:
public void popAll(Collection<? super E> dst) { while (!isEmpty()) dst.add(pop());}對于泛型wildcard的使用,作者指出:PECS,stands for producer-extends, consumer-super.即,類型參數表示一個生產者則使用<? extends T>,消費者則使用<? super T>。再舉個例子,比如我們要合并兩個集合的元素,由于這是生產行為,則聲明為:
public static <E> Set<E> union(Set<? extends E> s1,Set<? extends E> s2);那么能不能在返回類型中使用通配符? 作者建議盡量不要在返回類型中使用,因為這樣會讓調用變得復雜。 再來個稍微復雜一些的例子,比如我要在某個參數類型的List中找到最大的元素。 最初的聲明為:
public static <T extends Comparable<T>> T max(List<T> list)返回結果從list參數獲得,于是將參數聲明改為List<? extends T> list。那<T extends Comparable>有該如何處理。以java.util.concurrent中的ScheduledFuture和Delayed為例。(ps:interface ScheduledFuture extends Delayed 且 interface Delayed extends Comparable.)即,類型T本身沒有實現Comparable,但是他的父類實現了Comparable,于是聲明為:
public static <T extends Comparable<? super T> > T max(List<? extends T> list)最后還有一個有意思的例子,先看代碼:
public static void swap(List<?> list, int i, int j) { list.set(i, list.set(j, list.get(i)));}這段代碼是無法通過編譯的,因為無法將null以外的元素添加到List<?>中。當然,如果直接聲明一個類型參數就沒有問題,但現在假設我們只有使用通配符,并且不能使用raw type。既然知道通過類型參數可以解決,于是我們可以這樣:
public static void swap(List<?> list, int i, int j) { swapHelper(list, i, j);}private static <E> void swapHelper(List<E> list, int i, int j) { list.set(i, list.set(j, list.get(i)));}新聞熱點
疑難解答