Java中異常提供了一種識別及響應錯誤情況的一致性機制,有效地異常處理能使程序更加健壯、易于調試。
異常之所以是一種強大的調試手段,在于其回答了以下三個問題:
-
什么出了錯?
-
在哪出的錯?
-
為什么出錯?
在有效使用異常的情況下,異常類型回答了“什么”被拋出,異常堆棧跟蹤回答了“在哪“拋出,異常信息回答了“為什么“會拋出,如果你的異常沒有回答以上全部問題,那么可能你沒有很好地使用它們。
有三個原則可以幫助你在調試過程中最大限度地使用好異常,這三個原則是:
-
具體明確
-
提早拋出
-
延遲捕獲
為了闡述有效異常處理的這三個原則,本文通過杜撰個人財務管理器類JCheckbook進行討論,JCheckbook用于記錄及追蹤諸如存取款,票據(jù)開具之類的銀行賬戶活動。
具體明確
Java定義了一個異常類的層次結構,其以Throwable開始,擴展出Error和Exception,而Exception又擴展出RuntimeException.如圖1所示.
圖1.Java異常層次結構
這四個類是泛化的,并不提供多少出錯信息,雖然實例化這幾個類是語法上合法的(如:new Throwable()),但是最好還是把它們當虛基類看,使用它們更加特化的子類。Java已經(jīng)提供了大量異常子類,如需更加具體,你也可以定義自己的異常類。
例 如:java.io package包中定義了Exception類的子類IOException,更加特化確的是 FileNotFoundException,EOFException和ObjectStreamException這些IOException的子 類。
每一種都描述了一類特定的I/O錯誤:分別是文件丟失,異常文件結尾和錯誤的序列化對象流。異常越具體,我們的程序就能更好地回答”什么出了錯”這個問題。
捕獲異常時盡量明確也很重要。
例如:JCheckbook可以通過重新詢問用戶文件名來處理FileNotFoundException,對于 EOFException,它可以根據(jù)異常拋出前讀取的信息繼續(xù)運行。
如果拋出的是ObjectStreamException,則程序應該提示用戶文件 已損壞,應當使用備份文件或者其他文件。
Java讓明確捕獲異常變得容易,因為我們可以對同一try塊定義多個catch塊,從而對每種異常分別進行恰當?shù)奶幚怼?/p>
File prefsFile = new File(prefsFilename);
try{
readPreferences(prefsFile);
}
catch (FileNotFoundException e){
// alert the user that the specified file
// does not exist
}
catch (EOFException e){
// alert the user that the end of the file
// was reached
}
catch (ObjectStreamException e){
// alert the user that the file is corrupted
}
catch (IOException e){
// alert the user that some other I/O
// error occurred
}
JCheckbook 通過使用多個catch塊來給用戶提供捕獲到異常的明確信息。
舉例來說:如果捕獲了FileNotFoundException,它可以提示用戶指定另一 個文件,某些情況下多個catch塊帶來的額外編碼工作量可能是非必要的負擔,但在這個例子中,額外的代碼的確幫助程序提供了對用戶更友好的響應。
除前三個catch塊處理的異常之外,最后一個catch塊在IOException拋出時給用戶提供了更泛化的錯誤信息.這樣一來,程序就可以盡可能提供具體的信息,但也有能力處理未預料到的其他異常。
有時開發(fā)人員會捕獲范化異常,并顯示異常類名稱或者打印堆棧信息以求"具體"。
千萬別這么干!
用戶看到java.io.EOFException或者堆棧信息 只會頭疼而不是獲得幫助。應當捕獲具體的異常并且用"人話"給用戶提示確切的信息。不過,異常堆棧倒是可以在你的日志文件里打印。記住,異常和堆棧信息是用來幫助開發(fā)人 員而不是用戶的。
最后,應該注意到JCheckbook并沒有在readPreferences()中捕獲異常,而是將捕獲和處理異常留到用戶界面層來做,這樣就能用對話框或其他方式來通知用戶。這被稱為"延遲捕獲",下文就會談到。
提早拋出
異常堆棧信息提供了導致異常出現(xiàn)的方法調用鏈的精確順序,包括每個方法調用的類名,方法名,代碼文件名甚至行數(shù),以此來精確定位異常出現(xiàn)的現(xiàn)場。
java.lang.NullPointerException
at java.io.FileInputStream.open(Native Method)
at java.io.FileInputStream.
(FileInputStream.java:103) at jcheckbook.JCheckbook.readPreferences(JCheckbook.java:225)
at jcheckbook.JCheckbook.startup(JCheckbook.java:116)
at jcheckbook.JCheckbook.
(JCheckbook.java:27) at jcheckbook.JCheckbook.main(JCheckbook.java:318)
以上展示了FileInputStream類的open()方法拋出NullPointerException的情況。
不過注意 FileInputStream.close()是標準Java類庫的一部分,很可能導致這個異常的問題原因在于我們的代碼本身而不是Java API。所以問題很可能出現(xiàn)在前面的其中一個方法,幸好它也在堆棧信息中打印出來了。
不幸的是,NullPointerException是Java中信息量最少的(卻也是最常遭遇且讓人崩潰的)異常。它壓根不提我們最關心的事情:到底哪里是null。所以我們不得不回退幾步去找哪里出了錯。
通過逐步回退跟蹤堆棧信息并檢查代碼,我們可以確定錯誤原因是向readPreferences()傳入了一個空文件名參數(shù)。既然readPreferences()知道它不能處理空文件名,所以馬上檢查該條件:
public void readPreferences(String filename)
throws IllegalArgumentException{
if (filename == null){
throw new IllegalArgumentException("filename is null");
} //if
//...perform other operations...
InputStream in = new FileInputStream(filename);
//...read the preferences file...
}
通過提早拋出異常(又稱"迅速失?。ⅲ惓5靡郧逦譁蚀_。
堆棧信息立即反映出什么出了錯(提供了非法參數(shù)值),為什么出錯(文件名不能為空值),以及哪里出的錯(readPreferences()的前部分)。這樣我們的堆棧信息就能如實提供:
java.lang.IllegalArgumentException: filename is null
at jcheckbook.JCheckbook.readPreferences(JCheckbook.java:207)
at jcheckbook.JCheckbook.startup(JCheckbook.java:116)
at jcheckbook.JCheckbook.
(JCheckbook.java:27) at jcheckbook.JCheckbook.main(JCheckbook.java:318)
另外,其中包含的異常信息("文件名為空")通過明確回答什么為空這一問題使得異常提供的信息更加豐富,而這一答案是我們之前代碼中拋出的NullPointerException所無法提供的。
通過在檢測到錯誤時立刻拋出異常來實現(xiàn)迅速失敗,可以有效避免不必要的對象構造或資源占用,比如文件或網(wǎng)絡連接。同樣,打開這些資源所帶來的清理操作也可以省卻。
延遲捕獲
菜鳥和高手都可能犯的一個錯是,在程序有能力處理異常之前就捕獲它。Java編譯器通過要求檢查出的異常必須被捕獲或拋出而間接助長了這種行為。自然而然的做法就是立即將代碼用try塊包裝起來,并使用catch捕獲異常,以免編譯器報錯。
問題在于,捕獲之后該拿異常怎么辦?最不該做的就是什么都不做。
空的catch塊等于把整個異常丟進黑洞,能夠說明何時何處為何出錯的所有信息都會永遠丟失。把異常寫到日志中還稍微好點,至少還有記錄可查。
但我們總不能指望用戶去閱讀或者理解日志文件和異常信息。
讓readPreferences()顯示錯誤信息對話框也不合適,因為雖然JCheckbook目前是桌面應用程序,但我們還計劃將它變成基于HTML的Web應用。那樣的話,顯示錯誤對話框顯然不是個選擇。
同時,不管HTML還是C/S版本,配置信息都是在服務器上讀取的,而錯誤信息需要顯示給Web瀏覽器或者客戶端程序。 readPreferences()應當在設計時將這些未來需求也考慮在內。適當分離用戶界面代碼和程序邏輯可以提高我們代碼的可重用性。
在有條件處理異常之前過早捕獲它,通常會導致更嚴重的錯誤和其他異常。
例如,如果上文的readPreferences()方法在調用FileInputStream構造方法時立即捕獲和記錄可能拋出的FileNotFoundException,代碼會變成下面這樣:
public void readPreferences(String filename){
//...
InputStream in = null;
// DO NOT DO THIS!!!
try{
in = new FileInputStream(filename);
}
catch (FileNotFoundException e){
logger.log(e);
}
in.read(...);
//...
}
上面的代碼在完全沒有能力從FileNotFoundException中恢復過來的情況下就捕獲了它。如果文件無法找到,下面的方法顯然無法讀取它。
如果 readPreferences()被要求讀取不存在的文件時會發(fā)生什么情況?當然,F(xiàn)ileNotFoundException會被記錄下來,如果我們當時去看日志文件的話,就會知道。
然而當程序嘗試從文件中讀取數(shù)據(jù)時會發(fā)生什么?既然文件不存在,變量in就是空的,一個 NullPointerException就會被拋出。
調試程序時,本能告訴我們要看日志最后面的信息。那將會是NullPointerException,非常讓人討厭的是這個異常非常不具體。錯誤信息不僅誤導我們什么出了錯(真正的錯誤是FileNotFoundException而不是NullPointerException),還誤導了錯誤的出處。
真正的問題出在拋出NullPointerException處的數(shù)行之外,這之間有可能存在好幾次方法的調用和類的銷毀。我們的注意力被這條小魚從真正的錯誤處吸引了過來,一直到我們往回看日志才能發(fā)現(xiàn)問題的源頭。
既然readPreferences() 真正應該做的事情不是捕獲這些異常,那應該是什么?看起來有點有悖常理,通常最合適的做法其實是什么都不做,不要馬上捕獲異常。
把責任交給 readPreferences()的調用者,讓它來研究處理配置文件缺失的恰當方法,它有可能會提示用戶指定其他文件,或者使用默認值,實在不行的話也許警告用戶并退出程序。
把異常處理的責任往調用鏈的上游傳遞的辦法,就是在方法的throws子句聲明異常。在聲明可能拋出的異常時,注意越具體越好。這用于標識出調用你方法的程序需要知曉并且準備處理的異常類型。例如,“延遲捕獲”版本的readPreferences()可能是這樣的:
public void readPreferences(String filename)
throws IllegalArgumentException,
FileNotFoundException, IOException{
if (filename == null){
throw new IllegalArgumentException("filename is null");
} //if
//...
InputStream in = new FileInputStream(filename);
//...
}
技術上來說,我們唯一需要聲明的異常是IOException,但我們明確聲明了方法可能拋出FileNotFoundException。 IllegalArgumentException不是必須聲明的,因為它是非檢查性異常(即RuntimeException的子類)。然而聲明它是為 了文檔化我們的代碼(這些異常也應該在方法的JavaDocs中標注出來)。
當然,最終你的程序需要捕獲異常,否則會意外終止。
但這里的技巧是在合適的層面捕獲異常,以便你的程序要么可以從異常中有意義地恢復并繼續(xù)下去,而不導致更深入的錯誤;要么能夠為用戶提供明確的信息,包括引導他們從錯誤中恢復過來。如果你的方法無法勝任,那么就不要處理異常,把它留到后面捕獲和在恰當?shù)膶用嫣幚怼?/p>
結論
經(jīng)驗豐富的開發(fā)人員都知道,調試程序的最大難點不在于修復缺陷,而在于從海量的代碼中找出缺陷的藏身之處。只要遵循本文的三個原則,就能讓你的異常協(xié)助你跟蹤和消滅缺陷,使你的程序更加健壯,對用戶更加友好。
-
JAVA
+關注
關注
19文章
2952瀏覽量
104482
原文標題:有效處理 Java 異常三原則
文章出處:【微信號:mcuworld,微信公眾號:嵌入式資訊精選】歡迎添加關注!文章轉載請注明出處。
發(fā)布評論請先 登錄
相關推薦
評論