本文結合《Effective Java》第五章泛型和自己的理解及實踐,講解了Java泛型的知識點。文章發佈於專欄Effective Java,歡迎讀者訂閱。
你經常這樣寫代碼嗎
1、創建List時,刪掉ide幫你自動生成的尖括號,然後發現編譯器的警告,就按照ide的提示加上註解來消除警告
就像這樣
@SuppressWarnings("unchecked")
Set result = new HashSet(s1);
2、寫一個方法處理String類型的列表,入參是List<String>,然後下一次發現要處理Integer類型的列表,就再寫一個方法,入參改爲List<Integer>
如果你經常寫這樣的代碼,那麼,是時候好好學學Java泛型了。
什麼叫泛型(generic)
聲明中具有一個或者多個類型參數的類或者接口,就是泛型。
直觀來看,我們常用的List,
List<String> list = new ArrayList<>();
尖括號裏可以傳入一個類型參數,它是泛型;
再比如
Map<String,Object> = new HashMap<>()
尖括號裏可以傳入兩個類型參數,它也是泛型;
我們自己定義一個類,public Class MyGeneric<K,V,T>,這也是泛型。
爲什麼要使用泛型
其實,對於每一個泛型,都可以採用原生態類型(raw type)的方法去創建對象,即在定義的時候不帶任何實際類型參數,比如,我們使用原生態類型,創建一個存放郵票的集合
Collection stamps = ... ;
這時候,下面這句代碼是可以編譯通過的
stamps.add(new Coin());//往stamps集合放入了Coin類型的對象
接着,麻煩來了,我們在遍歷該集合時,理所當然的以爲,這個集合裏所有的對象都是Stamp類型
for( Iterator i = stamps.iterator();i.hasNext(); ) {
Stamp s = (Stamp)i.next();//throws ClassCastException
}
於是,當我們遍歷到那個濫竽充數的coin時,就會強轉失敗,拋出ClassCastException
還好,我們有泛型,我們使用泛型,重新定義stamps
Collection<Stamp> stamps = ... ;
這時候,編譯器就知道這個stamps裏面,只能包含Stamp對象,下面這句代碼,也就會在編譯時就報錯
stamps.add(new Coin());//編譯錯誤
而且在遍歷的時候,我們也就不需要強轉了
for( Iterator i = stamps.iterator();i.hasNext(); ) {
Stamp s = i.next();//無需強轉
}
總結一下,爲什麼要使用泛型?因爲,如果採用原生態類型,就會導致類型安全性問題,同時,在表述性方面也會變差。Java之所以還支持原生態類型,僅僅是出於兼容性考慮。
List list 和 List<Object>的區別
這兩種list,都表示 集合的元素可以是任何對象,那區別在哪呢,三行代碼告訴你差別
List<String> listString = new ArrayList<>();
List listRawType = listString;//編譯通過
List<Object> listObject = listString;//編譯失敗 Type mismatch: cannot convert from List<String> to List<Object>
也就是說,我們可以將List<String>的對象傳遞給List類型的參數,但是不能傳遞給List<Object>;
再往深了說,List<A>永遠不會是List<B>的子類型,泛型List之間不存在父子關係。
這一點和數組是不同的,假如 A extends B,那麼,A[] 就是 B[]的子類型,A[]可以傳遞給B[]類型的參數,但是,這些在List是不存在的。
再舉一個例子,這次引用書中的代碼
// Uses raw type (List) - fails at runtime! - Page 112
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);
}
這段代碼是可以編譯通過的,而在實際運行中,就會導致給List<String>中,插入了一個Integer類型的對象;
修改的方法就是在unsafeAdd方法聲明中用List<Object>代替List,這樣,編譯時就會無法通過。
爲什麼列表優先於數組
因爲數組是運行時類型安全,而列表是編譯時類型安全,可以在編譯時就避免錯誤。
什麼意思?舉個例子,採用數組,下面這段代碼編譯通過
Object[] objecArray = new Long[1];
objecArray[0] = "hey dude";
但是運行時,由於往一個long類型的數組插入字符串的元素,會拋出ArrayStoreException異常
如果使用List
List<Object> objectLIst = new ArrayList<Long>(); //編譯錯誤
objectLIst.add("hey dude");
第一行代碼編譯錯誤,原因和上面提到的一樣,List<Long>不是List<Object>的子類型
泛型方法
這一小節,我們來看看泛型實際運用中的一個例子,泛型方法。
這裏引用書中的例子,下面這個方法,返回兩個集合聯合後的集合
// Raw type method
public static Set unionUnSafe(Set s1, Set s2) {
Set result = new HashSet(s1);
result.addAll(s2);
return result;
}
這個方法很明顯不是類型安全的,調用者可以給方法傳遞元素類型不同的s1和s2,在編譯時會發現很多unchecked告警。 現在,我們來對這個方法進行泛型化改造
// Generic method
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<E>(s1);
result.addAll(s2);
return result;
}
這就是泛型方法,和普通方法不同的地方在於,要在方法修飾符和返回類型之間,聲明類型參數列表
public static <E> Set<E> union (Set<E> s1, Set<E> s2)
這樣的方法聲明,通過泛型解決了類型安全的問題,但是有一個侷限性,就是要求輸入的兩個集合的類型必須要相同。當然,我們很容易解決這個問題,比如你可以這樣聲明你的方法
public static <E extends T,K extends T,T> Set<T> union2(Set<E> s1, Set<K> s2) {
Set<T> result = new HashSet<T>(s1);
result.addAll(s2);
return result;
}
我們在類型參數列表中,聲明瞭E、K、T三個泛型,其中,E和K都是T的子類,E是s1的類型,K是s2的類型,T是返回值類型。
更優雅的,你還可以使用有限通配符類型來改造這個方法
public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2) {
Set<E> result = new HashSet<E>(s1);
result.addAll(s2);
return result;
}
關於有限通配符類型,這是下一小節要講的內容。
有限通配符
有限通配符是爲了解決什麼問題呢?
假設我們定義了下面這個方法,用來處理Number類型的列表
public static void processNumber(List<Number> numbers) {
for (Number num : numbers) {
System.out.println(num);
}
}
我們會理所當然的以爲,這個方法也可以處理Integer、Long等 Number子類的元素的集合,
然而,就像我們之前所說的,List<Integer>不是List<Number>的子類,
因此我們的願望落空了,下面這段代碼,連編譯都不通過
public static void main(String[] args) {
List<Integer> integers = Arrays.asList(1, 2, 3);
processNumber(integers);//The method processNumber(List<Number>) in the type GenericDemo1 is not applicable for the arguments (List<Integer>)
}
這個時候,就要用到有限通配符了
我們要做的,只是把processNumber方法的聲明改爲
public static void processNumber(List<? extends Number> numbers)
這個方法就可以滿足我們處理所以Number類型的List的需求。
既然有extends,那麼自然就會有super
public static void processNumber(List<? super AAA> numbers)
該方法可以處理子元素是AAA父類的集合
關於泛型中extends和super的用法,書中還提到了PECS法則,也即producer-extends,consumer-super,有興趣的同學可以去看看。
不要用通配符類型作爲返回類型
《Effective Java》指出,如果使用通配符類型作爲返回類型,那麼就會強制調用者在調用時使用通配符類型, 而通配符類型對於調用者來說應該是無形的, 通配符類型的作用是使方法能夠接受它們應該接受的參數,拒絕應該拒絕的參數。
其他
泛型擦除
簡單說就是,List<String> 在運行時會被擦除爲List
總結
爲什麼要使用泛型? 1、類型安全 2、提高API的通用性 這兩點,分別對應的文章開頭提出的兩個問題。
參考文獻
《Effective Java》第二版