咱們都知道,Java中異常提供了一種識別及響應錯誤狀況的一致性機制,有效地異常處理能使程序更加健壯、易於調試。異常之因此是一種強大的調試手段,在於其回答瞭如下三個問題:java
1.什麼出了錯?瀏覽器
2.在哪出的錯?服務器
3.爲何出錯?網絡
在有效使用異常的狀況下,異常類型回答了「什麼」被拋出,異常堆棧跟蹤回答了「在哪「拋出,異常信息回答了「爲何「會拋出,若是你的異常沒有回答以上所有問題,那麼可能你沒有很好地使用它們。有三個原則能夠幫助你在調試過程當中最大限度地使用好異常,這三個原則是:ide
1.具體明確編碼
2.提前拋出spa
3.延遲捕獲設計
爲了闡述有效異常處理的這三個原則,本文經過杜撰我的財務管理器類JCheckbook進行討論,JCheckbook用於記錄及追蹤諸如存取款,票據開具之類的銀行帳戶活動。調試
具體明確日誌
Java定義了一個異常類的層次結構,其以Throwable開始,擴展出Error和Exception,而Exception又擴展出RuntimeException.如圖1所示.
圖1.Java異常層次結構
這四個類是泛化的,並不提供多少出錯信息,雖然實例化這幾個類是語法上合法的(如:new Throwable()),可是最好仍是把它們當虛基類看,使用它們更加特化的子類。Java已經提供了大量異常子類,如需更加具體,你也能夠定義本身的異常類。
捕獲異常時儘可能明確也很重要。例如:JCheckbook能夠經過從新詢問用戶文件名來處理FileNotFoundException,對於 EOFException,它能夠根據異常拋出前讀取的信息繼續運行。若是拋出的是ObjectStreamException,則程序應該提示用戶文件 已損壞,應當使用備份文件或者其餘文件。
Java讓明確捕獲異常變得容易,由於咱們能夠對同一try塊定義多個catch塊,從而對每種異常分別進行恰當的處理。
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拋出時給用戶提供了更泛化的錯誤信息.這樣一來,程序就能夠儘量提供具體的信息,但也有能力處理未預料到的其餘異常。
有 時開發人員會捕獲範化異常,並顯示異常類名稱或者打印堆棧信息以求"具體"。千萬別這麼幹!用戶看到java.io.EOFException或者堆棧信息 只會頭疼而不是得到幫助。應當捕獲具體的異常而且用"人話"給用戶提示確切的信息。不過,異常堆棧卻是能夠在你的日誌文件裏打印。記住,異常和堆棧信息是用來幫助開發人 員而不是用戶的。
最後,應該注意到JCheckbook並無在readPreferences()中捕獲異常,而是將捕獲和處理異常留到用戶界面層來作,這樣就能用對話框或其餘方式來通知用戶。這被稱爲"延遲捕獲",下文就會談到。
提前拋出
異常堆棧信息提供了致使異常出現的方法調用鏈的精確順序,包括每一個方法調用的類名,方法名,代碼文件名甚至行數,以此來精肯定位異常出現的現場。
java.lang.NullPointerException
at java.io.FileInputStream.open(Native Method)
at java.io.FileInputStream.<init>(FileInputStream.java:103)
at jcheckbook.JCheckbook.readPreferences(JCheckbook.java:225)
at jcheckbook.JCheckbook.startup(JCheckbook.java:116)
at jcheckbook.JCheckbook.<init>(JCheckbook.java:27)
at jcheckbook.JCheckbook.main(JCheckbook.java:318)
以 上展現了FileInputStream類的open()方法拋出NullPointerException的狀況。不過注意 FileInputStream.close()是標準Java類庫的一部分,極可能致使這個異常的問題緣由在於咱們的代碼自己而不是Java API。因此問題極可能出如今前面的其中一個方法,幸虧它也在堆棧信息中打印出來了。
不幸的是,NullPointerException是Java中信息量最少的異常。它壓根不提咱們最關心的事情:到底哪裏是null。因此咱們不得不回退幾步去找哪裏出了錯。
經過逐步回退跟蹤堆棧信息並檢查代碼,咱們能夠肯定錯誤緣由是向readPreferences()傳入了一個空文件名參數。既然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...
}
經過提前拋出異常(又稱"迅速失敗"),異常得以清晰又準確。堆棧信息當即反映出什麼出了錯(提供了非法參數值),爲何出錯(文件名不能爲空值),以及哪裏出的錯(readPreferences()的前部分)。這樣咱們的堆棧信息就能如實提供:
java.lang.IllegalArgumentException: filename is null
at jcheckbook.JCheckbook.readPreferences(JCheckbook.java:207)
at jcheckbook.JCheckbook.startup(JCheckbook.java:116)
at jcheckbook.JCheckbook.<init>(JCheckbook.java:27)
at jcheckbook.JCheckbook.main(JCheckbook.java:318)
另外,其中包含的異常信息("文件名爲空")經過明確回答什麼爲空這一問題使得異常提供的信息更加豐富,而這一答案是咱們以前代碼中拋出的NullPointerException所沒法提供的。
經過在檢測到錯誤時馬上拋出異常來實現迅速失敗,能夠有效避免沒必要要的對象構造或資源佔用,好比文件或網絡鏈接。一樣,打開這些資源所帶來的清理操做也能夠省卻。
延遲捕獲
菜鳥和高手均可能犯的一個錯是,在程序有能力處理異常以前就捕獲它。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()被要求讀取不存在的文件時會發生什麼狀況?固然,FileNotFoundException會被記錄下來,若是咱們 當時去看日誌文件的話,就會知道。然而當程序嘗試從文件中讀取數據時會發生什麼?既然文件不存在,變量in就是空的,一個 NullPointerException就會被拋出。
調試程序時,本能告訴咱們要看日誌最後面的信息。那將會是NullPointerException,很是讓人討厭的是這個異常很是不具體。錯誤信息不只誤導咱們什麼出了錯(真正的錯誤是FileNotFoundException而不是NullPointerException),還誤導了錯誤的出處。真正 的問題出在拋出NullPointerException處的數行以外,這之間有可能存在好幾回方法的調用和類的銷燬。咱們的注意力被這條小魚從真正的錯誤處吸引了過來,一直到咱們往回看日誌才能發現問題的源頭。
既然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中標註出來)。
當 然,最終你的程序須要捕獲異常,不然會意外終止。但這裏的技巧是在合適的層面捕獲異常,以便你的程序要麼能夠從異常中有意義地恢復並繼續下去,而不致使更 深刻的錯誤;要麼可以爲用戶提供明確的信息,包括引導他們從錯誤中恢復過來。若是你的方法沒法勝任,那麼就不要處理異常,把它留到後面捕獲和在恰當的層面處理。
結論
經驗豐富的開發人員都知道,調試程序的最大難點不在於修復缺陷,而在於從海量的代碼中找出缺陷的藏身之處。只要遵循本文的三個原則,就能讓你的異常協助你跟蹤和消滅缺陷,使你的程序更加健壯,對用戶更加友好。
PS:若有任何問題,請直接在羣457036818提出。