BEAM 報(bào)告的結(jié)果文件是通過 build.xml 中 --beam::complaint_file
所定義的,在這里,本文假設(shè)其為 BEAM-messages。BEAM-messages
記錄著報(bào)出的所有代碼缺陷,這些缺陷分為 ERROR
,MISTAKE
和 WARNING
三大類,嚴(yán)重程度依次遞減。每一個(gè)具體的 ERROR
,MISTAKE
和 WARNING
都代表著一個(gè)錯(cuò)誤模式,本文接下來就通過實(shí)例分析理解其中的某些重要錯(cuò)誤模式,告訴讀者在寫 Java 代碼時(shí)如何避免這些錯(cuò)誤模式的發(fā)生,從而寫出高質(zhì)量的代碼。
由于篇幅原因,本文只主要重點(diǎn)介紹四個(gè)常見的錯(cuò)誤模式,并在最后簡單介紹一下在編程時(shí)還應(yīng)該注意的一些其它技巧,文章結(jié)構(gòu)如下:
這是報(bào)出的 ERROR2
錯(cuò)誤模式。據(jù)個(gè)人項(xiàng)目經(jīng)驗(yàn),這種錯(cuò)誤模式出現(xiàn)最為頻繁,但是編程人員卻往往很難發(fā)現(xiàn),因?yàn)檫@種編譯器發(fā)現(xiàn)不了的錯(cuò)誤可能在代碼運(yùn)行很長時(shí)間時(shí)都不會發(fā)生,可是一旦出現(xiàn),程序就會終止運(yùn)行,并拋出 runtime 異常 java.lang.NullPointerException
。通常有以下這些情況會導(dǎo)致操作空對象錯(cuò)誤模式的發(fā)生。
下面讓我們用簡單易懂的例子一一介紹它們。
清單 1. 調(diào)用空 String 對象的 charAt() 方法
這是最典型的調(diào)用空對象方法的例子,調(diào)用一個(gè)未初始化的 String
對象的 chatAt()
方法。
數(shù)組 array
的三個(gè) Integer
成員因?yàn)槌龜?shù)為 0 的異常并沒有被初始化(這里只是用典型的除數(shù)為 0 的異常舉例,其實(shí)實(shí)際工程中,初始化時(shí)發(fā)生的異常有時(shí)很難被發(fā)現(xiàn),沒有如此明顯),但是接下來仍然調(diào)用其第 0 個(gè)成員的 intValue()
方法。
總結(jié):調(diào)用空對象方法的錯(cuò)誤非常常見,導(dǎo)致其出現(xiàn)的原因通常有兩點(diǎn):
if
之類的條件不成立或者 for/while
循環(huán)的條件不成立,導(dǎo)致接下來的賦值動作并沒有進(jìn)行,其結(jié)果就是之前定義的空對象并沒有被初始化,然后又調(diào)用該對象的方法,從而造成了 java.lang.NullPointerException
,如清單 1 所示。 這種代碼缺陷在大型代碼工程中往往很難被發(fā)現(xiàn),因?yàn)榫幾g器不會報(bào)錯(cuò),而且代碼在實(shí)際運(yùn)行中,可能 99% 的時(shí)候 if
條件都是滿足的,初始化也是成功的,所以程序員很難在測試中發(fā)現(xiàn)該問題,但是這種代碼一旦交付到用戶手中,發(fā)現(xiàn)一次就是災(zāi)難性的。
建議的解決方法:一定要明確知道即將引用的對象是否是空對象。如果在某個(gè)方法中需要調(diào)用某個(gè)對象,而此對象又不是在本方法中定義(如:通過參數(shù)傳遞),這時(shí)就很難在此方法中明確知道此對象是否為空,那么一定要在調(diào)用此對象方法之前先判斷其是否為空,如果不為空,然后再調(diào)用其方法,如:if( obj != null ) { obj.method() … }
。
定義了某個(gè)類的對象,在沒有對其初始化之前就試圖訪問或修改其中的域,同樣會導(dǎo)致 java.lang.NullPointerException
異常。這種情況也非常常見,舉一個(gè)比較典型的數(shù)組對象的例子,如清單 3 所示:
數(shù)組 str
由于某些條件并沒有被初始化,但是卻訪問其 public final
域 length
想得到其長度。
總結(jié):訪問或修改某個(gè)空對象的域的起因與調(diào)用空對象的方法類似,通常是由于某些特殊情況導(dǎo)致原本應(yīng)該初始化的數(shù)組對象沒有被初始化,從而接下來訪問或修改其域時(shí)產(chǎn)生 java.lang.NullPointerException
異常。
建議的解決方法:與調(diào)用空對象的方法類似,盡量在訪問或修改某些不能夠明確判斷是否為空對象的域之前,對其進(jìn)行空對象判斷,從而避免對空對象的操作。
當(dāng)某個(gè)數(shù)組為空時(shí),試圖訪問或修改其數(shù)組元素時(shí)都會拋出 java.lang.NullPointerException
異常。
1 String[] str = null; 2 System.out.println( str[0]); 3 str[0] = "developerWorks" ; |
第 2 行和第 3 行都會導(dǎo)致 ERROR2 錯(cuò)誤,其中第 2 行試圖訪問空數(shù)組對象 str
的第 0 個(gè)元素,第 3 行試圖給空數(shù)組對象 str
的第 0 個(gè)元素賦值。
總結(jié):訪問或修改某個(gè)空數(shù)組對象的數(shù)組元素的起因與調(diào)用空對象的方法類似,通常是由于某些特殊情況導(dǎo)致原本應(yīng)該初始化的數(shù)組對象沒有被初始化,從而接下來訪問或修改其數(shù)組元素時(shí)產(chǎn)生 java.lang.NullPointerException
異常。
建議的解決方法:與調(diào)用空對象的方法類似,盡量在訪問或修改某些不能夠明確判斷是否為空空數(shù)組對象的數(shù)組元素之前,對其進(jìn)行空對象判斷,從而避免對空數(shù)組對象的操作。
對空對象 s
進(jìn)行同步。
總結(jié):同步空對象的起因與調(diào)用空對象的方法類似,通常是由于某些特殊情況導(dǎo)致原本應(yīng)該初始化的對象沒有被初始化,從而接下來導(dǎo)致同步空對象,并產(chǎn)生 java.lang.NullPointerException
異常。
建議的解決方法:與調(diào)用空對象的方法類似,盡量在同步某些不能夠明確判斷是否為空的對象之前,對其進(jìn)行空對象判斷,從而避免對空對象的操作。
將空 String
對象 string
傳入 getLength
方法,從而導(dǎo)致在 getLength
方法內(nèi)產(chǎn)生 java.lang.NullPointerException
異常。
總結(jié):導(dǎo)致傳入空對象參數(shù)的原因通常是在傳參前忘記對參數(shù)對象是否為空進(jìn)行檢查,或者調(diào)用了錯(cuò)誤的方法,或者假定接下來傳參的函數(shù)允許空對象參數(shù)。
建議的解決方法:如果函數(shù)的參數(shù)為對象,并且在函數(shù)體中需要操作該參數(shù)(如:訪問參數(shù)對象的方法或域,試圖修改參數(shù)對象的域等),一定要在函數(shù)開始處對參數(shù)是否為空對象進(jìn)行判斷,如果為空則不再執(zhí)行函數(shù)體,并最好作特殊處理,達(dá)到避免操作空對象的目的。
這是報(bào)出的 ERROR7
錯(cuò)誤模式。什么是數(shù)組訪問越界呢?如果一個(gè)數(shù)組(在 Java 中,Vector
,ArrayList
和 List
也算是數(shù)組類型)定義為有 n 個(gè)元素,那么對這 n 個(gè)元素(0~n-1)的訪問都是合法的,如果對這 n個(gè)元素之外的訪問,就是非法的,稱為“越界”。這種錯(cuò)誤同樣不會造成編譯錯(cuò)誤,會危險(xiǎn)地“埋伏”在你的程序中。在 C/C++中遇到數(shù)組訪問越界,可導(dǎo)致程序崩潰,甚至宕機(jī);在 Java 中,會拋出 runtime 異常 java.lang.ArrayIndexOutOfBoundsException
或 java.lang.IndexOutOfBoundsException
,并終止程序運(yùn)行。請看程序員容易犯的幾個(gè)典型數(shù)組訪問越界的例子:
int index = 2; String[] names = new String[] { "developer", "Works" }; System.out.println( names[index] ); |
index
為 2,而數(shù)組只有兩個(gè)元素,最后一個(gè)元素的下標(biāo)索引是 1,所以導(dǎo)致數(shù)組訪問越界。注意,如果 index
為負(fù)數(shù),仍然是數(shù)組訪問越界。
Vector<String> vec = new Vector<String>(); for ( int i = 0; i <= vec.size(); i ++ ) { System.out.println( vec.get(i) ); } |
Vector
和 ArrayList
的起始索引是 0,所以用其數(shù)組大小作為索引會導(dǎo)致數(shù)組訪問越界,其數(shù)組最后一個(gè)元素的索引應(yīng)該是“數(shù)組大小 -1 ”。
程序員調(diào)用 append
時(shí)以為數(shù)組 names
中有兩個(gè)元素,其實(shí)只有一個(gè)。
ArrayList
中最后一個(gè)元素已經(jīng)被 remove 了,所以該位置已經(jīng)沒有任何東西,訪問它將導(dǎo)致 java.lang.ArrayIndexOutOfBoundsException
。
總結(jié):導(dǎo)致數(shù)組訪問越界主要有以下幾個(gè)原因:
if
之類的條件不成立或者 for/while
循環(huán)的條件不成立,導(dǎo)致接下來的賦值動作并沒有進(jìn)行,從而接下來訪問了未初始化完全的數(shù)組,導(dǎo)致數(shù)組訪問越界,如清單 9 。 Vector
,ArrayList
或 List
中某些位置的元素已經(jīng)被 remove 了,后來仍然對該位置元素進(jìn)行訪問,可能會導(dǎo)致數(shù)組訪問越界,如清單 10 。 建議的解決方法:在判斷數(shù)組是否有效不為空的同時(shí),也要對訪問的數(shù)組元素的索引是否超出了上下限進(jìn)行檢查,如果索引是個(gè)變量,一定要確保變量取值在數(shù)組范圍之類(反例是清單 7);如果索引不是個(gè)變量,在確保索引正確的同時(shí)還要確保之前定義的數(shù)組足夠大(反例是清單 9)。最好是使用 try/catch
訪問數(shù)組,并對數(shù)組訪問越界異常進(jìn)行捕獲,進(jìn)行特殊處理,如清單 11 。
這是報(bào)出的 ERROR22
錯(cuò)誤模式。在 Java 中,如果除數(shù)為 0,會導(dǎo)致 runtime 異常 java.lang.ArithmeticException
并終止程序運(yùn)行,如清單 12 所示。
int num = 0; … int a = 5 / num; |
總結(jié):導(dǎo)致除 0 錯(cuò)誤的主要原因是使用變量作為除數(shù),并且程序員在寫除法語句時(shí),以為變量值到此已經(jīng)被改變(不是 0),但是實(shí)際上可能某條不被注意的語句路徑導(dǎo)致除數(shù)為 0,從而造成了錯(cuò)誤。
建議的解決方法:做除法前,一定不能將除數(shù)直接寫為 0 ;如果除數(shù)為變量,而且該變量值在進(jìn)行除法前經(jīng)過了很多運(yùn)算,導(dǎo)致不能確定在被除前是否為 0,則在除法前,先對除數(shù)變量進(jìn)行是否為 0 的判斷,并對除數(shù)為 0 的情況做特殊處理。
這是報(bào)出的ERROR23
錯(cuò)誤模式。內(nèi)存泄漏的后果非常嚴(yán)重,即使每次運(yùn)行只有少量內(nèi)存泄漏,但是長期運(yùn)行之后,系統(tǒng)仍然會面臨徹底崩潰的危險(xiǎn)。
在 C/C++ 中,內(nèi)存泄漏(MemoryLeak)一直是程序員特別頭疼的問題,因?yàn)樗鲥e(cuò)時(shí)的表現(xiàn)特征經(jīng)常很不穩(wěn)定(比如:錯(cuò)誤表象處不唯一,出錯(cuò)頻率不定等),而且出現(xiàn)問題的表象處經(jīng)常與內(nèi)存泄漏錯(cuò)誤代碼相隔甚遠(yuǎn),所以很難被定位查出。在 Java 中,垃圾回收器 (Garbage Collection,GC)的出現(xiàn)幫助程序員實(shí)現(xiàn)了自動管理內(nèi)存的回收,所以很多程序員認(rèn)為 Java不存在內(nèi)存泄漏問題,其實(shí)不然,垃圾回收器并不能解決所有的內(nèi)存泄漏問題,所以 Java 也存在內(nèi)存泄漏,只是表現(xiàn)與 C/C++ 不同。
為什么 Java 會出現(xiàn)內(nèi)存泄漏呢?因?yàn)槔厥掌髦换厥漳切┎辉俦灰玫膶ο?。但是有些對象的的確確是被引用的(可達(dá)的),但是卻無用的(程序以后不再使用這些對象),這時(shí)垃圾回收器不會回收這些對象,從而導(dǎo)致了內(nèi)存泄漏,拋出異常 java.lang.OutOfMemoryError
。以下是導(dǎo)致內(nèi)存泄漏的常見的例子(其中某些例子 BEAM 很難查出,這里列出只是為了給讀者提供一個(gè)反例進(jìn)行學(xué)習(xí))。
leakingHash
會往 Hashtable
中不停地加入元素,但是卻沒有相應(yīng)的移除動作(remove
),而且 static
的 Hashtable
永遠(yuǎn)都會貯存在內(nèi)存中,這樣必將導(dǎo)致 Hashtable
越來越大,最終內(nèi)存泄漏。
每次調(diào)用 leakingVector
都會少 remove
一個(gè) String
元素,如果 Vector
中的元素不是 String
,而是數(shù)據(jù)庫中一些非常大的記錄(record
),那么不停調(diào)用 leakingVector
將很快導(dǎo)致內(nèi)存耗光。
在BufferLeakDemo
對象的生命周期中,一直會有一個(gè) readBuffer 存在,其長度等于讀到的所有文件中最長文件的長度,而且更糟糕的是,該 readBuffer 只會增大,不會減小,所以如果不停的讀大文件,就會很快導(dǎo)致內(nèi)存泄漏。
文件輸出流 FileOutputStream 使用完了沒有關(guān)閉,導(dǎo)致 Stream 流相關(guān)的資源沒有被釋放,內(nèi)存泄漏。
如果 reader 讀取文件時(shí) InputStream 發(fā)生異常,那么 writer 將不會被關(guān)閉,從而導(dǎo)致內(nèi)存泄漏。
總結(jié):
Collection
類,如 Hashtable
,HashSet
,HashMap
,Vector
和 ArrayList
等,程序員使用時(shí)一般容易忘記 remove 不再需要的項(xiàng)(如清單 13),或者雖然 remove,但是 remove 的不干凈(如清單 14),這些都可能會導(dǎo)致無用的對象殘留在系統(tǒng)中,這樣的程序長時(shí)間運(yùn)行,可能會導(dǎo)致內(nèi)存泄漏。特別是當(dāng)這些 Collection
類的對象被聲明為 static 時(shí)或存活于整個(gè)程序生命周期時(shí),就更容易導(dǎo)致內(nèi)存泄漏。 Stream
流時(shí)(如 FileOutputStream
,PrintStream
等),創(chuàng)建并使用完畢后忘記關(guān)閉 close
(如清單 16),或者因?yàn)楫惓G闆r使得關(guān)閉 Stream
流的 close
的語句沒有被執(zhí)行(如清單 17),這些都會導(dǎo)致 Stream
流相關(guān)的資源沒有被釋放,從而產(chǎn)生內(nèi)存泄漏。 建議的解決方法:
null
,告訴垃圾回收器你已經(jīng)不再引用他們,從而垃圾回收器可以替你回收這些對象所占用的內(nèi)存空間。 Collection
類對象時(shí)(如 Hashtable
,HashSet
,HashMap
,Vector
和 ArrayList
等),如果可以,盡量定義其為局部變量,減少外界對其的引用,增大垃圾回收器回收他們的可能性。 Collection
類對象時(shí)(如 Hashtable
,HashSet
,HashMap
,Vector
和 ArrayList
等),注意手動 remove
其中不再使用的元素,減少垃圾對象的殘留。 event listener
),記住將不再需要監(jiān)聽的對象從監(jiān)聽列表中解除(remove
)。 Stream
流時(shí),一定要注意創(chuàng)建成功的所有 Stream
流一定要在使用完畢后 close
關(guān)閉,否則資源無法被釋放。 try
/ catch
語句中,添加 finally
聲明,對 try 中某些可能因?yàn)楫惓6鴽]釋放的資源進(jìn)行釋放。 finalize()
方法,手動對某些資源進(jìn)行垃圾回收。 hasNext()
后,再調(diào)用 next()
,而且不要在一個(gè) Iterator 的 hasNext()
成功后,去調(diào)用另外一個(gè) Iterator 的 next()
,如清單 18 。 Iterator firstnames = ( new Vector() ).iterator(); Iterator lastnames = ( new Vector() ).iterator(); while ( firstnames.hasNext() ) { //firstnames 中存在下一個(gè)元素,但 lastnames 可能已經(jīng)沒有元素了 String name = firstnames.next() + "." + lastnames.next(); } |
switch
語句中是否缺少 break 。有的時(shí)候程序員有意讓多個(gè) case 語句在一次執(zhí)行,但是有的時(shí)候卻是忘寫 break,導(dǎo)致發(fā)生了意想不到的結(jié)果,如清單 19 。 switch ( A ) { // 程序員原本的意思是 A 為 0 時(shí),B 為 0,A 為 1 時(shí),B 為 1,其實(shí) B 永遠(yuǎn)都不可能為 0 case 0: B = 0; case 1: B = 1; break; } |
例 1: if ( S.length() >= 0 ) // S 是 String 對象,它的長度永遠(yuǎn)大于等于 0,條件恒正確 例 2: // 程序員本來的意圖是想介于 MIN 和 MAX 之間的值才成立,卻誤將” && ”寫成” || ”,導(dǎo)致條件恒成立 if ( x >= MIN || x <= MAX ) 例 3: final boolean singleConnection = true; // final 型的 singleConnection 永遠(yuǎn)為 true,所以該條件恒成立,而且 connect() 永遠(yuǎn)不會被執(zhí)行 if ( singleConnection || connect() ) |
if ( S == “ d ” ) { … } else if ( S == “ e ” ) { … } else if ( S == “ v ” ) { … } else if ( S == “ e ” ) { … } // 少了 else 語句,漏掉的情況可能會產(chǎn)生異常,應(yīng)該加上 else 語句對剩下的條件進(jìn)行判斷和處理 |
switch
語句最好對所有的 case
進(jìn)行判斷,并且不要忘記對 default
情況進(jìn)行處理。