異常處理是 Java 開發中的一個重要部分,是為了處理任何錯誤狀況,比如資源不可通路,非法輸入,空輸入等等。Java 提供了幾個異常處理特性,以try,catch 和 finally 關鍵字的形式内建于語言自身之中。Java 程式設計語言也允許建立新的自定義異常,并通過使用 throw 和 throws關鍵字抛出它們。在Java程式設計中,Java 的異常處理不單單是知道文法這麼簡單,它必須遵循标準的 JDK 庫,和處理錯誤和異常的開源代碼。
這裡我們将讨論一些關于異常處理的 Java 最佳實踐。在我們讨論異常處理的最佳實踐之前,先讓我們了解下幾個重要的概念,那就是什麼是異常以及異常的分類。
什麼是異常?
異常的英文單詞是 exception,異常本質上是程式上的錯誤,包括程式邏輯錯誤和系統錯誤。比如使用空的引用、數組下标越界、記憶體溢出錯誤等,這些都是意外的情況,背離我們程式本身的意圖。錯誤在我們編寫程式的過程中會經常發生,包括編譯期間和運作期間的錯誤,在編譯期間出現的錯誤有編譯器幫助我們一起修正,然而運作期間的錯誤便不是編譯器力所能及了,并且運作期間的錯誤往往是難以預料的。假若程式在運作期間出現了錯誤,如果置之不理,程式便會終止或直接導緻系統崩潰,顯然這不是我們希望看到的結果。
如何對運作期間出現的錯誤進行處理和補救呢?Java 提供了異常機制來進行處理,通過異常機制來處理程式運作期間出現的錯誤。通過異常機制,我們可以更好地提升程式的健壯性。
異常分類
Java 把異常當作對象來處理,并定義一個基類 java.lang.Throwable 作為所有異常的超類。
Java 包括三種類型的異常: 檢查性異常(checked exceptions)、非檢查性異常(unchecked Exceptions) 和錯誤(errors)。
- 檢查性異常(checked exceptions) 是必須在在方法的 throws 子句中聲明的異常。它們擴充了異常,旨在成為一種“在你面前”的異常類型。JAVA希望你能夠處理它們,因為它們以某種方式依賴于程式之外的外部因素。檢查的異常表示在正常系統操作期間可能發生的預期問題。 當你嘗試通過網絡或檔案系統使用外部系統時,通常會發生這些異常。 大多數情況下,對檢查性異常的正确響應應該是稍後重試,或者提示使用者修改其輸入。
- 非檢查性異常(unchecked Exceptions) 是不需要在throws子句中聲明的異常。 由于程式錯誤,JVM并不會強制你處理它們,因為它們大多數是在運作時生成的。 它們擴充了 RuntimeException。 最常見的例子是 NullPointerException, 未經檢查的異常可能不應該重試,正确的操作通常應該是什麼都不做,并讓它從你的方法和執行堆棧中出來。
- 錯誤(errors) 是嚴重的運作時環境問題,肯定無法恢複。 例如 OutOfMemoryError,LinkageError 和 StackOverflowError,通常會讓程式崩潰。
所有不是 Runtime Exception 的異常,統稱為 Checked Exception,又被稱為檢查性異常。這類異常的産生不是程式本身的問題,通常由外界因素造成的。為了預防這些異常産生時,造成程式的中斷或得到不正确的結果,Java 要求編寫可能産生這類異常的程式代碼時,一定要去做異常的處理。
Java 語言将派生于 RuntimeException 類或 Error 類的所有異常稱為非檢查性異常。
Java 異常層次結構圖如下圖所示:

在了解了異常的基本概念以及分類後,現在讓我們開始探索異常處理的最佳實踐吧。
異常處理最佳實踐
不要忽略捕捉的異常
catch (NoSuchMethodException e) {
return null;
}
雖然捕捉了異常但是卻沒有做任何處理,除非你确信這個異常可以忽略,不然不應該這樣做。這樣會導緻外面無法知曉該方法發生了錯誤,無法确定定位錯誤原因。
在你的方法裡抛出定義具體的檢查性異常
public void foo() throws Exception { //錯誤方式
}
一定要避免出現上面的代碼示例,它破壞了檢查性異常的目的。 聲明你的方法可能抛出的具體檢查性異常,如果隻有太多這樣的檢查性異常,你應該把它們包裝在你自己的異常中,并在異常消息中添加資訊。 如果可能的話,你也可以考慮代碼重構。
public void foo() throws SpecificException1, SpecificException2 { //正确方式
}
捕獲具體的子類而不是捕獲 Exception 類
try {
someMethod();
} catch (Exception e) { //錯誤方式
LOGGER.error("method has failed", e);
}
捕獲異常的問題是,如果稍後調用的方法為其方法聲明添加了新的檢查性異常,則開發人員的意圖是應該處理具體的新異常。如果你的代碼隻是捕獲異常(或 Throwable),永遠不會知道這個變化,以及你的代碼現在是錯誤的,并且可能會在運作時的任何時候中斷。
永遠不要捕獲 Throwable 類
這是一個更嚴重的麻煩,因為 Java Error 也是 Throwable 的子類,Error 是 JVM 本身無法處理的不可逆轉的條件,對于某些 JVM 的實作,JVM 可能實際上甚至不會在 Error 上調用 catch 子句。
始終正确包裝自定義異常中的異常,以便堆棧跟蹤不會丢失
catch (NoSuchMethodException e) {
throw new MyServiceException("Some information: " + e.getMessage()); //錯誤方式
}
這破壞了原始異常的堆棧跟蹤,并且始終是錯誤的,正确的做法是:
catch (NoSuchMethodException e) {
throw new MyServiceException("Some information: " , e); //正确方式
}
要麼記錄異常要麼抛出異常,但不要一起執行
catch (NoSuchMethodException e) {
//錯誤方式
LOGGER.error("Some information", e);
throw e;
}
正如上面的代碼中,記錄和抛出異常會在日志檔案中産生多條日志消息,代碼中存在單個問題,并且對嘗試分析日志的同僚很不友好。
finally 塊中永遠不要抛出任何異常
try {
someMethod(); //Throws exceptionOne
} finally {
cleanUp(); //如果finally還抛出異常,那麼exceptionOne将永遠丢失
}
隻要 cleanUp() 永遠不會抛出任何異常,上面的代碼沒有問題,但是如果 someMethod() 抛出一個異常,并且在 finally 塊中,cleanUp() 也抛出另一個異常,那麼程式隻會把第二個異常抛出來,原來的第一個異常(正确的原因)将永遠丢失。如果在 finally 塊中調用的代碼可能會引發異常,請確定要麼處理它,要麼将其記錄下來。永遠不要讓它從 finally 塊中抛出來。
始終隻捕獲實際可處理的異常
catch (NoSuchMethodException e) {
throw e; //避免這種情況,因為它沒有任何幫助
}
這是最重要的概念,不要為了捕捉異常而捕捉,隻有在想要處理異常時才捕捉異常,或者希望在該異常中提供其他上下文資訊。如果你不能在 catch 塊中處理它,那麼最好的建議就是不要隻為了重新抛出它而捕獲它。
不要使用 printStackTrace() 語句或類似的方法
完成代碼後,切勿忽略 printStackTrace(),最終别人可能會得到這些堆棧,并且對于如何處理它完全沒有任何方法,因為它不會附加任何上下文資訊。
對于不打算處理的異常,直接使用 finally
try {
someMethod(); //Method 2
} finally {
cleanUp(); //do cleanup here
}
這是一個很好的做法,如果在你的方法中你正在通路 Method 2,而 Method 2 抛出一些你不想在 Method 1 中處理的異常,但是仍然希望在發生異常時進行一些清理,然後在 finally 塊中進行清理,不要使用 catch 塊。
記住早 throw 晚 catch 原則
這可能是關于異常處理最著名的原則,簡單說,應該盡快抛出(throw)異常,并盡可能晚地捕獲(catch)它。應該等到有足夠的資訊來妥善處理它。
這個原則隐含地說,你将更有可能把它放在低級方法中,在那裡你将檢查單個值是否為空或不适合。而且你會讓異常堆棧跟蹤上升好幾個級别,直到達到足夠的抽象級别才能處理問題。
在異常處理後清理資源
如果你正在使用資料庫連接配接或網絡連接配接等資源,請確定清除它們。如果你正在調用的 API 僅使用非檢查性異常,則仍應使用 try-finally 塊來清理資源。 在 try 子產品裡面通路資源,在 finally 裡面最後關閉資源。即使在通路資源時發生任何異常,資源也會優雅地關閉。
隻抛出和方法相關的異常
相關性對于保持應用程式清潔非常重要。一種嘗試讀取檔案的方法,如果抛出 NullPointerException,那麼它不會給使用者任何相關的資訊。相反,如果這種異常被包裹在自定義異常中,則會更好。NoSuchFileFoundException 則對該方法的使用者更有用。
切勿在程式中使用異常來進行流程控制
不要在項目中出現使用異常來處理應用程式邏輯。永遠不要這樣做,它會使代碼很難閱讀和了解。
盡早驗證使用者輸入以在請求處理的早期捕獲異常
始終要在非常早的階段驗證使用者輸入,甚至在達到 controller 之前,它将幫助你把核心應用程式邏輯中的異常處理代碼量降到最低。如果使用者輸入出現錯誤,還可以保證與應用程式一緻。
例如:如果在使用者注冊應用程式中,遵循以下邏輯:
- 驗證使用者
- 插入使用者
- 驗證位址
- 插入位址
- 如果出問題復原一切
這是不正确的做法,它會使資料庫在各種情況下處于不一緻的狀态,應該首先驗證所有内容,然後将使用者資料置于 dao 層并進行資料庫更新。正确的做法是:
- 如果問題復原一切
一個異常隻能包含在一個日志中
LOGGER.debug("Using cache sector A");
LOGGER.debug("Using retry sector B");
不要像上面這樣做,對多個 LOGGER.debug() 調用使用多行日志消息可能在你的測試用例中看起來不錯,但是當它在具有 100 個并行運作的線程的應用程式伺服器的日志檔案中顯示時,所有資訊都輸出到相同的日志檔案,即使它們在實際代碼中為前後行,但是在日志檔案中這兩個日志消息可能會間隔 100 多行。應該這樣做:
LOGGER.debug("Using cache sector A, using retry sector B");
将所有相關資訊盡可能地傳遞給異常
有用的異常消息和堆棧跟蹤非常重要,如果你的日志不能定位異常位置,那要日志有什麼用呢?
終止掉被中斷線程
while (true) {
try {
Thread.sleep(100000);
} catch (InterruptedException e) {} //别這樣做
doSomethingCool();
}
InterruptedException 異常提示應該停止程式正在做的事情,比如事務逾時或線程池被關閉等。
應該盡最大努力完成正在做的事情,并完成目前執行的線程,而不是忽略 InterruptedException。修改後的程式如下:
while (true) {
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
break;
}
}
doSomethingCool();
對于重複的 try-catch,使用模闆方法
在代碼中有許多類似的 catch 塊是無用的,隻會增加代碼的重複性,針對這樣的問題可以使用模闆方法。
例如,在嘗試關閉資料庫連接配接時的異常處理。
class DBUtil{
public static void closeConnection(Connection conn){
try{
conn.close();
} catch(Exception ex){
//Log Exception - Cannot close connection
}
}
}
這類的方法将在應用程式很多地方使用。不要把這塊代碼放的到處都是,而是定義上面的方法,然後像下面這樣使用它:
public void dataAccessCode() {
Connection conn = null;
try{
conn = getConnection();
....
} finally{
DBUtil.closeConnection(conn);
}
}
使用 JavaDoc 中記錄應用程式中的所有異常
把用 JavaDoc 記錄運作時可能抛出的所有異常作為一種習慣,其中也盡量包括使用者應該遵循的操作,以防這些異常發生。
總結
這篇文章首先介紹了什麼是異常,以及異常的三種分類,然後通過 20 個最佳實踐來讨論如何處理異常,希望能在以後異常處理的時候有所改進及感悟。