1. 程式人生 > >如何優雅的處理異常(java)?——來自知乎答案

如何優雅的處理異常(java)?——來自知乎答案


Java中異常提供了一種識別及響應錯誤情況的一致性機制,有效地異常處理能使程式更加健壯、易於除錯。異常之所以是一種強大的除錯手段,在於其回答了以下三個問題:

  • 什麼出了錯?
  • 在哪出的錯?
  • 為什麼出錯?

在有效使用異常的情況下,異常型別回答了“什麼”被丟擲,異常堆疊跟蹤回答了“在哪“丟擲,異常資訊回答了“為什麼“會丟擲,如果你的異常沒有回答以上全部問題,那麼可能你沒有很好地使用它們。有三個原則可以幫助你在除錯過程中最大限度地使用好異常,這三個原則是:

  • 具體明確
  • 提早丟擲
  • 延遲捕獲

為了闡述有效異常處理的這三個原則,本文通過杜撰個人財務管理器類JCheckbook進行討論,JCheckbook用於記錄及追蹤諸如存取款,票據開具之類的銀行賬戶活動。
具體明確


Java定義了一個異常類的層次結構,其以Throwable開始,擴展出Error和Exception,而Exception又擴展出RuntimeException.如圖1所示.

<img src="https://pic3.zhimg.com/acad623aaaabcf49dd856ab264984176_b.jpg" data-rawwidth="300" data-rawheight="204" class="content_image" width="300">

圖1.Java異常層次結構

這四個類是泛化的,並不提供多少出錯資訊,雖然例項化這幾個類是語法上合法的(如:new Throwable()),但是最好還是把它們當虛基類看,使用它們更加特化的子類。Java已經提供了大量異常子類,如需更加具體,你也可以定義自己的異常類。

例 如:java.io package包中定義了Exception類的子類IOException,更加特化確的是 FileNotFoundException,EOFException和ObjectStreamException這些IOException的子 類。每一種都描述了一類特定的I/O錯誤:分別是檔案丟失,異常檔案結尾和錯誤的序列化物件流.異常越具體,我們的程式就能更好地回答”什麼出了錯”這個 問題。

捕 獲異常時儘量明確也很重要。例如: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中標註出來)。

當 然,最終你的程式需要捕獲異常,否則會意外終止。但這裡的技巧是在合適的層面捕獲異常,以便你的程式要麼可以從異常中有意義地恢復並繼續下去,而不導致更 深入的錯誤;要麼能夠為使用者提供明確的資訊,包括引導他們從錯誤中恢復過來。如果你的方法無法勝任,那麼就不要處理異常,把它留到後面捕獲和在恰當的層面處理。
結論
經驗豐富的開發人員都知道,除錯程式的最大難點不在於修復缺陷,而在於從海量的程式碼中找出缺陷的藏身之處。只要遵循本文的三個原則,就能讓你的異常協助你跟蹤和消滅缺陷,使你的程式更加健壯,對使用者更加友好。

原文請看:http://www.importnew.com/1701.html

作者:ylxfc
連結:https://www.zhihu.com/question/28254987/answer/40173231
來源:知乎
著作權歸作者所有,轉載請聯絡作者獲得授權。