二十三、請不要在新代碼中使用原生態類型:
?? ?? 先簡單介紹一下泛型的概念和聲明形式。聲明中具有一個或者多個類型參數的類或者接口,就是泛型類或接口,如List<E>,這其中E表示List集合中元素的類型。在Java中,相對于每個泛型類都有一個原生類與之對應,即不帶任何實際類型參數的泛型名稱,如List<E>的原生類型List。他們之間最為明顯的區別在于List<E>包含的元素必須是E(泛型)類型,如List<String>,那么他的元素一定是String,否則將產生編譯錯誤。和泛型不同的是,原生類型List可以包含任何類型的元素,因此在向集合插入元素時,即使插入了不同類型的元素也不會引起編譯期錯誤。那么在運行,當List的使用從List中取出元素時,將不得不針對類型作出判斷,以保證在進行元素類型轉換時不會拋出ClassCastException異常。由此可以看出,泛型集合List<E>不僅可以在編譯期發現該類錯誤,而且在取出元素時不需要再進行類型判斷,從而提高了程序的運行時效率。
????? 以上僅為簡化后的示例代碼,當run()方法中拋出異常時,可以很快發現是在main()中添加了非Stamp類型的元素。如果給stamps對象添加元素的操作是在多個函數或線程中完成的,那么迅速定位到底是哪個或哪幾個函數添加了非Stamp類型的元素,將會需要更多的時間去調試。
? ? ? 通過以上兩個例子可以看出泛型類型相對于原生類型還是有著非常明顯的優勢的。一般而言,原生類型的使用都是為了保持一定的兼容性,畢竟泛型是在Java 1.5中才推出的。如原有的代碼中(Java 1.5之前)包含一個函數,其參數為原生類型,如void func(List l); 在之后的升級代碼中,如果給該函數傳入泛型類型的List<E>對象將是合法的,不會產生編譯錯誤。同時Java的泛型對象在運行時也會被擦除類型,即List<E>擦除類型后將會變成List,Java之所以這樣實現也就是為了保持向后的兼容性。
? ? ? 現在我們比較一下List和List<Object>這兩個類型之間的主要區別,盡管這兩個集合可以包含任何類型的對象元素,但是前者是類型不安全的,而后者則明確告訴使用者可以存放任意類型的對象元素。另一個區別是,如果void func(List l)改為void func(List<Object> l),List<String>類型的對象將不能傳遞給func函數,因為Java將這兩個泛型類型視為完全不同的兩個類型。
? ? ? 在新代碼中不要使用原生類型,這條規則有兩個例外,兩者都源于“泛型信息可以在運行時被擦除”這一事實。在Class對象中必須要使用原生類型。JLS不允許使用Class的參數化類型。換句話說,List.class, String[].class和int.class都是合法的,但是List<String>.class和List<?>.class則是不合法。這條規則的第二個例外與instanceof操作符相關。由于泛型信息可以在運行時被擦除,因此在泛型類型上使用instanceof操作符是非法的。如:
1 private void test(Set o) { 2 if (o instanceof Set) { 3 Set<?> m = (Set<?>)o; 4 } 5 }
二十四、消除非受檢警告:
? ? ? 在進行泛型編程時,經常會遇到編譯器報出的非受檢警告(unchecked cast warnings),如:Set<Lark> exaltation = new HashSet(); 對于這樣的警告要盡可能在編譯期予以消除。對于一些比較難以消除的非受檢警告,可以通過@SuppressWarnings("unchecked")注解來禁止該警告,前提是你已經對該條語句進行了認真地分析,確認運行期的類型轉換不會拋出ClassCastException異常。同時要在盡可能小的范圍了應用該注解(SuppressWarnings),如果可以應用于變量,就不要應用于函數。盡可能不要將該注解應用于Class,這樣極其容易掩蓋一些可能引發異常的轉換。見如下代碼:
?? ?? 編譯該代碼片段時,編譯器會針對(T[])Arrays.copyOf(elements,size,a.getClass())語句產生一條非受檢警告,現在我們需要做的就是添加一個新的變量,并在定義該變量時加入@SuppressWarnings注解,見如下修訂代碼:
?? ?? 這個方法可以正確的編譯,禁止非受檢警告的范圍也減少到了最小。
?? ?? 為什么要消除非受檢警告,還有一個比較重要的原因。在開始的時候,如果工程中存在大量的未消除非受檢警告,開發者認真分析了每一處警告并確認不會產生任何運行時錯誤,然而所差的是在分析之后沒有消除這些警告。那么在之后的開發中,一旦有新的警告發生,極有可能淹沒在原有的警告中,而沒有被開發者及時發現,最終成為問題的隱患。如果恰恰相反,在分析之后消除了所有的警告,那么當有新警告出現時將會立即引起開發者的注意。
?? ?
二十五、列表優先于數組:
?? ?? 數組和泛型相比,有兩個重要的不同點。首先就是數組是協變的,如:Object[] objArray = new Long[10]是合法的,因為Long是Object的子類,與之相反,泛型是不可協變的,如List<Object> objList = new List<Long>()是非法的,將無法通過編譯。因此泛型可以保證更為嚴格的類型安全性,一旦出現插入元素和容器聲明時不匹配的現象是,將會在編譯期報錯。二者的另一個區別是數組是具體化的,因此數組會在運行時才知道并檢查它們的元素類型約束。如將一個String對象存儲在Long的數組中時,就會得到一個ArrayStoreException異常。相比之下,泛型則是通過擦除來實現的。因此泛型只是在編譯時強化類型信息,并在運行時丟棄它們的元素類型信息。擦除就是使泛型可以與沒有使用泛型的代碼隨意進行交互。由此可以得出混合使用泛型和數組是比較危險的,因為Java的編譯器禁止了這樣的使用方法,一旦使用,將會報編譯錯誤。見如下用例:
? ? ? 從以上示例得出,當你得到泛型數組創建錯誤時,最好的解決辦法通常是優先使用集合類型List<E>,而不是數組類型E[]。這樣可能會損失一些性能或簡潔性,但是換回的卻是更高的類型安全性和互用性。見如下示例代碼:
? ? ? 事實上,從以上函數和接口的定義可以看出,如果他們被定義成泛型函數和泛型接口,將會得到更好的類型安全,同時也沒有對他們的功能造成任何影響,見如下修改為泛型的示例代碼:
?? ?? 這樣的寫法回提示一個編譯錯誤,即E[] snapshot = l.toArray();是無法直接轉換并賦值的。修改方式也很簡單,直接強轉就可以了,如E[] snapshot = (E[])l.toArray();在強轉之后,仍然會收到編譯器給出的一條警告信息,即無法在運行時檢查轉換的安全性。盡管結果證明這樣的修改之后是可以正常運行的,但是這樣的寫法確實也是不安全的,更好的辦法是通過List<E>替換E[],見如下修改后的代碼:
二十六、優先考慮泛型:
?? ?? 如下代碼定義了一個非泛型集合類:
????? 在看與之相對于的泛型集合實現方式:
????? 上面的泛型集合類Stack<E>在編譯時會引發一個編譯錯誤,即elements = new E[DEFAULT_INITIAL_CAPACITY]語句不能直接實例化泛型該類型的對象。修改方式如下:elements = (E[])new Object[DEFAULT_INITIAL_CAPACITY],只要我們保證所有push到該數組中的對象均為該類型的對象即可,剩下需要做的就是添加注解以消除該警告:
1 @SuppressWarning("unchecked") 2 public Stack() { 3 elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; 4 }
????? 總而言之,使用泛型比使用需要在客戶端代碼中進行轉換的類型來的更加安全,也更加容易。在設計新類型的時候,要確保它們不需要這種轉換就可以使用。這通常意味著要把類做成是泛型的。
?? ?
二十七、優先考慮泛型方法:
?? ?? 和優先選用泛型類一樣,我們也應該優先選用泛型方法。特別是靜態工具方法尤其適合于范興華。如Collections.sort()和Collections.binarySearch()等靜態方法。見如下非泛型方法:
1 public static Set union(Set s1, Set s2) { 2 Set result = new HashSet(s1); 3 result.addAll(s2); 4 return result; 5 }
????? 這個方法在編譯時會有警告報出。為了修正這些警告,最好的方法就是使該方法變為類型安全的,要將方法聲明修改為聲明一個類型參數,表示這三個集合的元素類型,并在方法中使用類型參數,見如下修改后的泛型方法代碼:
1 public static <E> Set<E> union(Set<E> s1,Set<E> s2) { 2 Set<E> result = new HashSet<E>(s1); 3 result.addAll(s2); 4 return result; 5 }
????? 和調用泛型對象構造函數來創建泛型對象不同的是,在調用泛型函數時無須指定函數的參數類型,而是通過Java編譯器的類型推演來填充該類型信息,見如下泛型對象的構造:
?? ?? Map<String,List<String>> anagrams = new HashMap<String,List<String>>();
?? ?? 很明顯,以上代碼在等號的兩邊都顯示的給出了類型參數,并且必須是一致的。為了消除這種重復,可以編寫一個泛型靜態工廠方法,與想要使用的每個構造器相對應,如:
1 public static <K,V> HashMap<K,V> newHashMap() { 2 return new HashMap<K,V>(); 3 }
?? ?? 我們的調用方式也可以改為:Map<String,List<String>> anagrams = newHashMap();
?? ?? 除了在以上的情形下使用泛型函數之外,我們還可以在泛型單例工廠的模式中應用泛型函數,這些函數通常為無狀態的,且不直接操作泛型對象的方法,見如下示例:
????? 調用方式如下:
????? 對于該靜態函數,如果我們為類型參數添加更多的限制條件,如參數類型必須是Comparable<T>的實現類,這樣我們的函數對象便可以基于該接口做更多的操作,而不僅僅是像上例中只是簡單的返回參數對象,見如下代碼:
????? 總而言之,泛型方法就想泛型對象一樣,提供了更為安全的使用方式。
?? ?
二十八、利用有限制通配符來提升API的靈活性:
?? ?? 前面的條目已經解釋為什么泛型不支持協變,而在我們的實際應用中可能確實需要一種針對類型參數的特化,幸運的是,Java提供了一種特殊的參數化類型,稱為有限制的通配符類型(bounded wildcard type),來處理類似的情況。見如下代碼:
1 public class Stack<E> { 2 public Stack(); 3 public void push(E e); 4 public E pop(); 5 public boolean isEmpty(); 6 }
?? ?? 現在我們需要增加一個方法:
1 public void pushAll(Iterable<E> src) { 2 for (E e : src) 3 push(e); 4 }
?? ?? 如果我們的E類型為Number,而我們卻喜歡將Integer對象也插入到該容器中,現在的寫法將會導致編譯錯誤,因為即使Integer是Number的子類,由于類型參數是不可變的,因此這樣的寫法也是錯誤的。需要進行如下的修改:
1 public void pushAll(Iterable<? extends E> src) { 2 for (E e : src) 3 push(e); 4 }
?? ?? 修改之后該方法便可以順利通過編譯了。因為參數中Iterable的類型參數被限制為E(Number)的子類型即可。
?? ?? 既然有了pushAll方法,我們可能也需要新增一個popAll的方法與之對應,見如下代碼:
1 public void popAll(Collection<E> dst) { 2 while (!isEmpty()) 3 dst.add(pop()); 4 }
? ? ? popAll方法將當前容器中的元素全部彈出,并以此添加到參數集合中。如果Collections中的類型參數和Stack完全一致,這樣的寫法不會有任何問題,然而在實際的應用中,我們通常會將Collection中的元素視為更通用的對象類型,如Object,見如下應用代碼:
? ? ? Stack<Number> numberStack = new Stack<Number>();
? ? ? Collection<Object> objs = createNewObjectCollection();
? ? ? numberStack.popAll(objs);
? ? ? 這樣的應用方法將會導致編譯錯誤,因為Object和Stack中Number參數類型是不匹配的,而我們對目標容器中對象是否為Number并不關心,Object就已經滿足我們的需求了。為了到達這種更高的抽象,我們需要對popAll做如下的修改:
1 public void popAll(Collection<? super E> dst) { 2 while (!isEmpty()) 3 dst.add(pop()); 4 }
?? ?? 修改之后,之前的使用方式就可以順利通過編譯了。因為參數集合的類型參數已經被修改為E(Number)的超類即可。
? ? ? 這里給出了一個助記方式,便于我們記住需要使用哪種通配符類型:
? ? ?
?
PECS(producer-extends, consumer-super)
? ? ? 解釋一下,如果參數化類型表示一個T生產者,就使用<? extends T>,如果它表示一個T消費者,就使用<? super T>。在我們上面的例子中,pushAll的src參數產生E實例供Stack使用,因此src相應的類型為Iterable<? extends E>;popAll的dst參數通過Stack消費E實例,因此dst相應的類型為Collection<? super E>。PECS這個助記符突出了使用通配符類型的基本原則。
? ? ? 在上一個條目中給出了下面的泛型示例函數:
? ? ?
?
public static <E> Set<E> union(Set<E> s1, Set<E> s2);
? ? ? 這里的s1和s2都是生產者,根據PECS原則,它們的聲明可以改為:
?? ??
?
public static <E> Set<E> union(Set<? extends E> s1,Set<? extends E> s2);
? ? ? 由于泛型函數在調用時,其參數類型是可以通過函數參數的類型推演出來的,如果上面的函數被如下方式調用時,將會導致Java的編譯器無法推演出泛型參數的實際類型,因此引發了編譯錯誤。
?? ?? Set<Integer> integers = new Set<Integer>();
?? ?? Set<Double> doubles = new Set<Double>();
? ? ? Set<Number> numbers = union(integers,doubles);
? ? ? 如果想順利通過編譯并得到正確的執行結果,我們只能通過顯示的方式指定該函數類型參數的實際類型,從而避免了編譯器的類型參數自動推演,見修改后的代碼:
? ? ? Set<Number> numbers = Union.<Number>union(integers,doubles);
?? ?
? ? ? 現在我們再來看一下前面也給出過的max方法,其初始聲明為:
? ? ? public static <T extends Comparable<T>> T max<List<T> srcList);
? ? ? 下面是修改過的使用通配符類的聲明:
?? ?? public static <T extends Comparable<? super T>> T max(List<? extends T> srcList);
? ? ? 下面將逐一給出新聲明的解釋:
? ? ? 1.?? ?函數參數srcList產生了T實例,因此將類型從List<T>改為List<? extends T>;
?? ?? 2.?? ?最初T被指定為擴展Comparable<T>,然而Comparable又是T的消費者,用于比較兩個T之間的順序關系。因此參數化類型Comparable<T>被替換為Comparable<? super T>。
? ? ?
?
注:Comparator和Comparable一樣,他們始終都是消費者,因此Comparable<? super T>優先于Comparable<T>。
二十九、優先考慮類型安全的異構容器:
?? ?? 泛型通常用于集合,如Set和Map等。這樣的用法也就限制了每個容器只能有固定數目的類型參數,一般來說,這也確實是我們想要的。然而有的時候我們需要更多的靈活性,如數據庫可以用任意多的Column,如果能以類型安全的方式訪問所有Columns就好了,幸運的是有一種方法可以很容易的做到這一點,就是將key進行參數化,而不是將容器參數化,見以下代碼:
1 public class Favorites { 2 public <T> void putFavorite(Class<T> type,T instance); 3 public <T> T getFavorite(Class<T> type); 4 }
? ? ? 下面是該類的使用示例:
? ? ? 這里Favorites實例是類型安全的:當你請求String的時候,它是不會給你Integer的。同時它也是異構的容器,不像普通的Map,他的所有鍵都是不同類型的。下面就是Favorites的具體實現:
?? ?? 可以看出每個Favorites實例都得到一個Map<Class<?>,Object>容器的支持。由于該容器的值類型為Object,為了進一步確實類型的安全性,我們在put的時候通過Class.cast()方法將Object參數嘗試轉換為Class所表示的類型,如果類型不匹配,將會拋出ClassCastException異常。以此同時,在從Map中取出值對象的時候,由于該對象當前的類型是Object,因此我們需要再次利用Class.cast()函數將其轉換為我們的目標類型。
? ? ? 對于Favorites類的put/get方法,有一個非常明顯的限制,即我們無法將“不可具體化”類型存入到該異構容器中,如List<String>、List<Integer>等泛型類型。這樣的限制主要源于Java中泛型類型在運行時的類型擦出機制,即List<String>.class和List<Integer>.class是等同的對象,均為List.class。如果Java編譯器通過了這樣的調用代碼,那么List<String>.class和List<Integer>.class將會返回相同的對象引用,從而破壞Favorites的內部結構。
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
