人非圣賢,孰能無(wú)過(guò)。都說(shuō)Java語(yǔ)言是一門簡(jiǎn)單的編程語(yǔ)言,基于C++演化而來(lái),剔除了很多C++中的復(fù)雜特性,但這并不能保證Java程序員不會(huì)犯錯(cuò)。那么對(duì)于廣大的Java程序員來(lái)說(shuō),它們最常犯的個(gè)錯(cuò)誤是什么呢?本文通過(guò)總結(jié)出Java程序員最常犯的大錯(cuò)誤,可以有效地幫組Java后來(lái)者少走彎路,少加班,并寫出更健壯的應(yīng)用程序。
1. 數(shù)組轉(zhuǎn)ArrayList
為了實(shí)現(xiàn)把一個(gè)數(shù)組轉(zhuǎn)換成一個(gè)ArrayList,很多Java程序員會(huì)使用如下的代碼:
Arrays.asList確實(shí)會(huì)返回一個(gè)ArrayList對(duì)象,但是該類是Arrays類 中一個(gè)私有靜態(tài)內(nèi)部類,而不是常見的java.util.ArrayList類。這個(gè)java.util.Arrays.ArrayList類具有 set(),get(),contains()等方法,但是不具有任何添加或移除元素的任何方法。因?yàn)樵擃惖拇笮?size)是固定的。為了創(chuàng)建出一個(gè)真正的ArrayList,代碼應(yīng)該如下所示:
我們知道,ArrayList的構(gòu)造方法可以接受一個(gè)Collection類型的對(duì)象,而我們的 java.util.Arrays.ArrayList正好也是它的一個(gè)子類。實(shí)際上,更加高效的代碼示例是:
2. 數(shù)組是否包含特定值
為了檢查數(shù)組中是否包含某個(gè)特定值,很多Java程序員會(huì)使用如下的代碼:
就功能而言,該代碼是正確無(wú)誤的,但在數(shù)組轉(zhuǎn)List,List再轉(zhuǎn)Set的過(guò)程中消耗了大量的性能。我們可以優(yōu)化成如下形式:
或者,進(jìn)一步優(yōu)化成如下所示最高效的代碼:
3. 在迭代時(shí)移除List中的元素
首先,看一下在迭代過(guò)程中移除List中元素的代碼:
這個(gè)示例代碼的輸出結(jié)果是:
這個(gè)示例代碼中存在一個(gè)非常嚴(yán)重的錯(cuò)誤。當(dāng)一個(gè)元素被移除時(shí),該List的大小(size)就會(huì)縮減,同時(shí)也改變了索引的指向。所以,在迭代的過(guò)程中使用索引,將無(wú)法從List中正確地刪除多個(gè)指定的元素。
你可能知道解決這個(gè)錯(cuò)誤的方式之一是使用迭代器(iterator)。而且,你可能認(rèn)為Java中的foreach語(yǔ)句與迭代器(iterator)是非常相似的,但實(shí)際情況并不是這樣。我們考慮一下如下的示例代碼:
這個(gè)示例代碼會(huì)拋出來(lái)一個(gè)ConcurrentModificationException。我們應(yīng)該修改成如下所示:
next()方法必須在remove()方法之前被調(diào)用。在 foreach循環(huán)中,編譯器使得 remove()方法先于next()方法被調(diào)用,這就導(dǎo)致了ConcurrentModificationException 異常。具體細(xì)節(jié)可以查看ArrayList.iterator()的源碼。
4. Hashtable vs HashMap
學(xué)習(xí)過(guò)數(shù)據(jù)結(jié)構(gòu)的讀者都知道一種非常重要的數(shù)據(jù)結(jié)構(gòu)叫做哈希表。在Java中,對(duì)應(yīng)哈希表的的類是HashMap而不是Hashtable。HashMap與Hashtable之間的最核心區(qū)別就是:HashMap是非同步的,Hashtable是同步的。
5. 在Collection中使用原始類型
在Java中,很容易把原始類型與無(wú)限通配類型混淆。我們舉個(gè)Set相關(guān)的例子:Set就是原始類型;Set就是無(wú)限通配類型。我們看一個(gè)使用在List中使用原始類型的例子:
這個(gè)示例代碼會(huì)拋出來(lái)一個(gè)異常:
在Collection使用原始類型是具有很多的類型錯(cuò)誤風(fēng)險(xiǎn)的,因?yàn)樵碱愋蜎](méi)有靜態(tài)類型檢查。實(shí)際上,Set、Set和Set之間具有非常大的差異。
6. 訪問(wèn)權(quán)限
很多的Java初學(xué)者喜歡使用public來(lái)修飾類的成員。這樣可以很方便地直接訪問(wèn)和存取該成員。但是,這是一種非常糟糕的編程風(fēng)格,正確的設(shè)計(jì)風(fēng)格應(yīng)該是盡可能降低類成員的訪問(wèn)權(quán)限。
7. ArrayList vs LinkedList
很多的Java初學(xué)者不明白ArrayList與LinkedList之間的區(qū)別,所以,他們完全只用相對(duì)簡(jiǎn)單的ArrayList,甚至不知道JDK中還存在LinkedList。但是,在某些具體場(chǎng)景下,這兩種List的選擇會(huì)導(dǎo)致程序性能的巨大差異。簡(jiǎn)單而言:當(dāng)應(yīng)用場(chǎng)景中有很多的add/remove操作,只有少量的隨機(jī)訪問(wèn)操作時(shí),應(yīng)該選擇LinkedList;在其他的場(chǎng)景下,考慮使用ArrayList。
8. 可變 vs 不可變
不可變的對(duì)象具有非常多的優(yōu)勢(shì),比如簡(jiǎn)單,安全等。但是,對(duì)于每一個(gè)不同的值,都需要該類的一個(gè)對(duì)象。而且,生成很多對(duì)象帶來(lái)的問(wèn)題就是可能導(dǎo)致頻繁的垃圾回收。所以,在選擇可變類還是不可變類時(shí),應(yīng)該綜合考慮后再做抉擇。
通常而言,可變對(duì)象可以避免創(chuàng)建大量的中間對(duì)象。一個(gè)非常經(jīng)典的例子就是鏈接大量的短String對(duì)象為一個(gè)長(zhǎng)的String對(duì)象。如果使用不可變String類,鏈接的過(guò)程將產(chǎn)生大量的,適合立即被垃圾回收的中間String對(duì)象,這將消耗大量的CPU性能和內(nèi)存空間。此時(shí),使用一個(gè)可變的StringBuilder或StringBuffer才是正確的。
除了上述情況,可變對(duì)象在其他場(chǎng)景下可能用于不可變對(duì)象。比如,傳遞一個(gè)可變的對(duì)象到方法內(nèi)部,利用該對(duì)象可以收集多個(gè)結(jié)果,而不用在多個(gè)循環(huán)層次中跳進(jìn)跳出。
9. 繼承中的構(gòu)造函數(shù)
上圖中出現(xiàn)的兩個(gè)編譯時(shí)錯(cuò)誤是因?yàn)椋焊割愔袥](méi)有定義默認(rèn)構(gòu)造函數(shù),而子類中又調(diào)用了父類的默認(rèn)構(gòu)造函數(shù)。在Java中,如果一個(gè)類不定義任何構(gòu)造函數(shù),編譯期將自動(dòng)插入一個(gè)默認(rèn)構(gòu)造函數(shù)到給類中。一旦一個(gè)類定義了任何一個(gè)構(gòu)造函數(shù),編譯期就不會(huì)插入任何構(gòu)造函數(shù)到類中。在上面的示例中,Super類定義了一個(gè)參數(shù)類型為String的構(gòu)造函數(shù),所以該類中只有一個(gè)構(gòu)造函數(shù),不會(huì)有默認(rèn)構(gòu)造函數(shù)了。
&emps;在我們的子類 Sub 中,我們定義了兩個(gè)構(gòu)造函數(shù):一個(gè)參數(shù)類型為String的構(gòu)造函數(shù),另一個(gè)為午餐的默認(rèn)函數(shù)。由于它們都沒(méi)有在函數(shù)體的第一行指定調(diào)用父類的哪一個(gè)構(gòu)造函數(shù),所以它們都需要調(diào)用父類 Super 的默認(rèn)構(gòu)造函數(shù)。但是,父類 Super 的默認(rèn)構(gòu)造函數(shù)是不存在的,所以編譯器報(bào)告了這兩個(gè)錯(cuò)誤信息。
10. 字符串對(duì)象的兩個(gè)構(gòu)建方式
Java中的字符串對(duì)象具有兩個(gè)常見的創(chuàng)建方式:
它們之間的區(qū)別是什么呢?我們?cè)倏匆幌氯缦碌拇a: