1. 程式人生 > >JAVA 中異常處理的最佳實踐

JAVA 中異常處理的最佳實踐

前言

異常處理的問題之一是知道何時以及如何去使用它。我會討論一些異常處理的最佳實踐,也會總結最近在異常處理上的一些爭論。

作為程式設計師,我們想要寫高質量的能夠解決問題的程式碼。但是,異常經常是伴隨著程式碼產生的副作用。沒有人喜歡副作用,因此我們會試圖用自己的方式來解決這個問題。我看過不少的程式用下面的方法應對異常:

上面這段程式碼的問題在哪裡?

一旦一個異常被丟擲之後,正常的執行流程會停止並且將控制交給捕捉塊。捕捉塊捕獲異常,然後只是把它的資訊列印了一下。之後程式正常執行,就像沒有任何事情發生一樣。

那下面的這種方法呢?

這是一個空方法,裡面沒有任何的程式碼。為什麼一個空方法能夠丟擲異常?JAVA並不阻止你這麼做。最近,我遇到了一些和這個很相似的程式碼,明明程式碼塊中沒有丟擲異常的語句,卻在方法宣告中丟擲異常。當我問開發人員為什麼這麼做,他會回答“我知道這樣會影響API,但是我之前就這麼做的而且效果還不錯”。

C 社群花了好久才決定如何使用異常。這場爭論也在JAVA社群產生了。我看到不少JAVA開發人員艱難的使用異常。如果不能夠正確使用的話,異常會影響程式的效能,因為它需要使用記憶體和CPU來建立,丟擲以及捕獲。如果過度使用的話,會使得程式碼難以閱讀,並且影響API的使用人員。我們都知道這將會帶來程式碼漏洞以及壞味道。客戶端程式碼常會通過忽略這個異常或是直接將其丟擲來避開這個問題,就像之前的兩個例子那樣。

小編是一個有著5年工作經驗的java程式設計師,對於java,自己有做資料的整合,一個完整學習java的路線,學習資料和工具,相信這裡有很多學習java的小夥伴,我創立了一個2000人學習扣群,479121291。每晚都有java的直播課程。無論是初級還是進階的小夥伴小編我都歡迎!

異常的本質

從廣義的角度來說,一共有三種不同的場景會導致異常的產生:

程式設計錯誤導致的異常:這一類的異常是因為不恰當的程式設計帶來的(比如 , )。客戶端通常無法對這些錯誤採取任何措施

客戶端程式碼的錯誤:客戶端程式碼在API允許的範圍之外使用API,從而違背了合約。客戶端可以通過異常中提供的有用資訊,採用一些替代方法。比如,當解析格式不正確的XML檔案時,會丟擲異常。這個異常中包含導致該錯誤發生的XML內容的具體位置。客戶端可以通過這些資訊採取回覆措施。

資源失效導致的異常:比如系統記憶體不足或是網路連線失敗。客戶端面對資源失效的迴應是要根據上下文來決定的。客戶端可以在一段時間之後試著重新連線或是記錄資源失效日誌然後暫停應用程式。

JAVA異常型別

JAVA定義了兩種異常:

需檢查的異常:從 類繼承的異常都是需檢查異常。客戶端需要處理API丟擲的這一類異常,通過try-catch或是繼續丟擲。

無需檢查的異常: 也是 的子類。但是,繼承了 的類受到了特殊的待遇。客戶端程式碼無需專門處理這一類異常。

下圖展示了 的繼承樹:

上圖中, 繼承自 ,因此它也是一個無需檢查的異常。

我看到過大量使用需檢查異常只在極少數時候使用無需檢查異常的。最近,JAVA社群在需檢查異常的真正價值上爆發了熱烈的討論。這場辯論源於JAVA是第一個包含需檢查異常的主流OO框架。C 和C#根本沒有需檢查異常。這些語言中所有的異常都是無需檢查的。

從低層丟擲的需檢查異常強制要求呼叫方捕獲或是丟擲該異常。如果客戶端不能有效的處理該異常,API和客戶端之間的異常協議將會帶來極大的負擔。客戶端的開發人員可能會通過將異常抑制在一個空的捕獲塊中或是直接丟擲它。從而又將這個負擔交給了客戶端的呼叫方。

還有人指責需檢查異常會破壞封裝,看下面這段程式碼:

方法丟擲了兩個需檢查異常。呼叫這個方法的客戶端必須明確的處理這兩種具體的異常,即使它們並不清楚 內究竟是哪個檔案訪問或是資料庫訪問失敗了,而且它們也沒有提供檔案系統或是資料庫的邏輯。因此,這樣的異常處理導致方法和呼叫者之前出現了不當的強耦合。

設計API的最佳實踐

在討論了這些之後,我們可以來探討一下如何設計一個正確丟擲異常的良好的API。

1.在選擇丟擲需確定異常或是無需確定異常時,問自己這樣的一個問題:客戶端程式碼在遇到異常時會進行怎樣的處理?

如果客戶端能夠採取措施從這個異常中恢復過來,那就選擇需確定異常。如果客戶端不能採取有效的措施,就選擇無需確定異常。有效的措施是指從異常中恢復的措施,而不僅僅是記錄錯誤日誌。

除此以外,儘量選擇無需確定的異常:它的優點在於不會強迫客戶端顯式地處理這種異常。它會冒泡到任何你想捕獲它的地方。JAVA API提供了許多無需檢查的異常如 , 和 。我傾向於使用JAVA提供的標準的異常,儘量不去建立自己的異常。

2.保留封裝

永遠不要將特定於實現的異常傳遞到更高層。比如,不要將資料層的 傳遞出去。業務層不需要了解 。你有兩個選擇:

將 轉換為另一個需檢查異常,如果客戶程式碼需要從異常中恢復。

將 轉換為無需檢查異常,如果客戶端程式碼無法對其進行處理。

大多數時候,客戶程式碼無法解決 。這時候就將其轉化為無需檢查的異常。

這裡的catch塊並沒有做任何事情。不如通過如下的方式解決它:

這裡將 轉化為了 。如果 出現了,catch塊就會丟擲一個執行時異常。當前執行的執行緒將會停止並報告該異常。但是,該異常並沒有影響到我的業務邏輯模組,它無需進行異常處理,更何況它根本無法對 進行任何操作。如果我的catch塊需要根異常原因,可以使用 方法。

如果你確信業務層可以採取補救措施,你可以將其轉化為一個更有意義的無需檢查異常。但是我覺得丟擲RuntimeException足以適用大多數的場景。

3.當無法提供更加有用資訊時,不要自定義異常

下面這段程式碼有什麼問題?

它沒有給客戶端程式碼提供任何有用的資訊,除了一個稍微具有含義的命名。不要忘了 類和別的類一樣,在裡面你可以新增一下方法供客戶端呼叫,獲得有用的資訊。

新版本的異常提供了兩個有用的方法: ,它會返回請求的名字,和 ,它會返回一組相近的可用的使用者名稱。客戶端可以使用這些方法來獲取有用的資訊。但是如果你不準備新增這些額外的資訊,那就丟擲一個標準的異常即可。

如果你覺得客戶端程式碼在記錄日誌之外對這個異常不能進行任何操作,那麼最好丟擲無需檢查異常:

除此以外,你還可以提供一個方法來檢查使用者名稱是否已經被使用。

4.文件化異常

你可以使用Javadoc的 標記來記錄需檢查異常和無需檢查異常。但是,我傾向於寫單元測試來文件化異常。單元測試允許我在使用中檢視異常,並且作為一個可以被執行的文件來使用。無論你採用哪種方法,儘量使你的客戶端程式碼瞭解你的API會丟擲的異常。這裡提供了 的單元測試。

上面這段程式碼在呼叫 應當丟擲 。如果沒有丟擲該異常,則會執行 顯式的說明該測試失敗了。通過為異常編寫測試,你不僅能記錄異常如何觸發,而且使你的程式碼在經過這些測試後更加健壯。

使用異常的最佳實踐

1.自覺清理資源

如果你在使用如資料庫連線或是網路連線之類的資源,要確保你及時的清理這些資源。如果你呼叫的API僅僅出發了無需檢查異常,你仍然需要在使用後主動清理。使用 塊。

類關閉 連線。這裡的重點在於在 塊中關閉連線,無論是否出現了異常。

2.永遠不要使用異常來控制流

生成棧追蹤的代價很昂貴,它的價值在於debug過程中使用。在一個流程控制中,棧追蹤應當被忽視,因為客戶端只想知道如何進行。

在下面的程式碼中, 被用來進行流程控制:

通過無限迴圈來增加計數,直到丟擲異常。這種方式使得程式碼難以閱讀,而且影響程式碼效能。只在出現異常的場景丟擲異常。

3.不要無視或是壓制異常

當API的方法會丟擲異常的時候,它在提醒你應當採取一些措施。如果需檢查異常沒有任何意義,那就乾脆將其轉化為無需檢查異常再重新丟擲。不要單純的用catch捕獲它然後繼續執行,彷彿什麼都沒有發生一樣。

4.不要捕獲最高層異常

繼承 的異常同樣是 的子類。捕獲 的同時,也捕獲了執行時異常:

5.只記錄異常一次

將同一個異常多次記入日誌會使得檢查追蹤棧的開發人員感到困惑,不知道何處是報錯的根源。所以只記錄一次。

覺得本文對你有幫助?請分享給更多人。