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

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