異常機制是現代主流語言的標配,但是異常處理問題雖然已經被討論很多,也有很多經典書籍的論述,卻一直都充滿爭議。很多人都覺得異常處理很難拿捏,同時也難以理解一些語言或庫的異常處理設計。我使用Java近10年,但直到最近我才感覺完全理清了對於異常處理的種種疑惑,下面就介紹一下我對Java異常處理原理和原則的一些認識,歡迎交流探討!
Exception和Error的區別
談異常處理的第一個問題是:什么是異常?什么又不是異常?這個問題看似簡單,其實很多人都沒有分辨清楚,尤其是人們經常混用異常(Exception)、錯誤(Error)、失敗(Failure)、缺陷(Bug)這些接近又有區別的詞。
這里最需要對比區分的是Failure/Exception和Bug/Error。用例子來說,你嘗試打開一個文件失敗了,觸發了一個IOException
,這是一種運行時遇到的操作失敗,它並不代表你的程序本身有問題。但是,Bug就不一樣了,假設你有一個sort
函數對數組進行排序,如果發現調用sort
之后居然還有亂序情況,導致整個系統行為出錯最后crash,那么這就不是異常而是錯誤,唯一的解決辦法是修改程序解決Bug。所以,在Java中我們可以這樣區分,異常(Exception)是一種非程序原因的操作失敗(Failure),而錯誤(Error)則意味着程序有缺陷(Bug)。注意:其他語言術語可能不同,重要的是能從概念上區分它們。
Java的類繼承體系非常清楚地區分了Exception和Error。
java.lang.Object
java.lang.Throwable
java.lang.Error
java.lang.Exception
Exception下面是我們常見的各種異常類,Error下面最著名的就是AssertionError
,它可以通過throw new AssertionError(...)
顯式拋出,也可以通過assert
操作符產生。Java文檔中明確說到:
An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch.
就是說一般情況下不應該嘗試用catch(Throwable)
或者catch(Error)
去捕獲Error,因為拋出這個Error就是希望整個程序馬上停下來。可能有人會疑惑:“如果不捕獲Error,程序crash了后果很嚴重啊”?這個就要靠自己結合具體情況去判斷了,讓程序帶着已經發作的Bug跑還是立刻停下來,到底哪個后果更嚴重?有時是前者,有時是后者。
聲明異常和未聲明異常的區別
Java可以在方法簽名上顯式地聲明可能拋出的異常,但也允許拋出某些未聲明的異常。那么,二者有何區別呢?我們自己在設計一個方法時如何決定是否在方法上聲明某個異常呢?本質上講,在方法簽名上聲明的異常屬於方法接口的一部分,它和方法的返回值處於同一抽象層次,不隨具體實現的變化而改變。比如,Integer類用於解析一個字符串到Integer型整數的valueOf方法:
public static Integer valueOf(String s) throws NumberFormatException
它聲明拋出的NumberFormatException
屬於這個方法接口層面的一種失敗情況,不管內部實現采用什么解析方法,都必然存在輸入字符串不是合法整數這種情況,所以這時把這個異常聲明出來就非常合理。相反,下面這個從帳戶a向帳戶b轉賬的transfer
方法:
public boolean transfer(Account a, Account b, Money money) throws SQLException
它拋出SQLException
就不對了,因為SQLException
不屬於這個transfer
接口層面的概念,而屬於具體實現,很有可能未來某個實現不用SQL了那么這個異常也就不存在了。這種情況下,就應該捕獲SQLException
,然后拋出自定義異常TransferException
,其中TransferException
可以定義幾種和業務相關的典型錯誤情況,比如金額不足,帳戶失效,通信故障,同時它還可以引用SQLException
作為觸發原因(Cause)。
public boolean transfer(Account a, Account b, Money money) throws TransferException {
try {
...
executeSQL(...);
} catch (SQLException e) {
throw new TransferException("...", e);
}
}
什么情況下方法應該拋出未聲明的異常?
前面談到在編寫一個方法時,聲明異常屬於接口的一部分,不隨着具體實現而改變,但是我們知道Java允許拋出未聲明的RuntimeException
,那么什么情況下會這樣做呢?比如,下面的例子中方法f
聲明了FException
,但是它的實現中可能拋出RuntimeException
,這是什么意思呢?
void f() throws FException {
if (...) {
throw new RuntimeException("...");
}
}
根據上面提到的原理,未聲明異常是和實現相關的,有可能隨着不同實現而出現或消失,同時它又對應不到FException。比如,f
方法依賴於對象a
,結果在運行時a
居然是null
,導致本方法無法完成相應功能,這就可以成為一種未聲明的RuntimeException
了(當然,更常見的是直接調用a
的方法,然后觸發NullPointerException
)。
其實,很多情況下拋出未聲明的RuntimeException
的語義和Error非常接近,只是沒有Error那么強烈,方法的使用者可以根據情況來處理,不是一定要停止整個程序。我們最常見的RuntimeException
可能要算NullPointerException
了,通常都是程序Bug引起的,如果是C/C++就已經crash了,Java給了你一個選擇如何處理的機會。
所以,拋出未聲明異常表示遇到了和具體實現相關的運行時錯誤,它不是在設計時就考慮到的方法接口的一部分,所以又被稱為是不可恢復的異常。有些Java程序員為了簡便不聲明異常而直接拋出RuntimeException的做法從設計上是不可取的。
如何捕獲和處理其他方法拋出的異常?
下面例子中方法g
聲明了GException
,方法f
聲明了FException
,而f
在調用g
的時候不管三七二十一通過catch (Exception e)
捕獲了所有的異常。
void g() throws GException
void f() throws FException {
try {
g();
} catch (Exception e) {
...
}
....
}
這種做法是很多人的習慣性寫法,它的問題在哪里呢?問題就在於g
明明已經告訴你除了GException
外,如果拋出未聲明的RuntimeException就表示遇到了錯誤,很可能是程序有Bug,這時f
還不顧一切繼續帶着Bug跑。所以,除非有特殊理由,對具體情況做了分析判斷,一般不捕獲未聲明異常,讓它直接拋出就行。
void g() throws GException
void f() throws FException {
try {
g();
} catch (GException e) {
...
}
....
}
但是,很遺憾有一種很典型的情況是g()
是不受自己控制的代碼,它雖然只聲明了拋出GException
,實際上在實現的時候拋出了未聲明的但屬於接口層面而應該聲明的異常。如果遇到這種情況最好的做法是應該告訴g()
的作者修改程序聲明出這些異常,如果實在不行也只能全部捕獲了。但是,對於自己寫的程序來講,一定要嚴格區別聲明異常和未聲明異常的處理,這樣做的目的是理清Exception和Bug的界限。
自定義異常應繼承Exception還是RuntimeException?
Java中區分Checked Exception和Unchecked Exception,前者繼承於Exception
,后者繼承於RuntimeException
。Unchecked Exception和Runtime Exception在Java中常常是指同一個意思。
public boolean createNewFile() throws IOException
上面的IOException
就是一個著名的Checked Exception。Java編譯器對Checked Exception的約束包括兩方面:對於方法編寫者來講,Checked Exception必須在方法簽名上聲明;對於方法調用者來講,調用拋出Checked Exception的方法必須用try-catch捕獲異常或者繼續聲明拋出。相反,Unchecked Exception則不需要顯式聲明,也不強制捕獲。
Checked Exception的用意在於明確地提醒調用者去處理它,防止遺漏。但是Checked Exception同時也給調用者帶來了負擔,通常會導致層層的try-catch,降低代碼的可讀性,前面例子中的Integer.valueOf
方法雖然聲明了NumberFormatException
,但是它是一個RuntimeException
,所以使用者不是必須用try-catch去捕獲它。
實際上,對於自己編寫的異常類來講,推薦默認的是繼承RuntimeException,除非有特殊理由才繼承Exception。C#中沒有Checked Exception的概念,這種推薦的做法等於是采用了C#的設計理念:把是否捕獲和何時捕獲這個問題交給使用者決定,不強制使用者。當然,如果某些情況下明確提醒捕獲更加重要還是可以采用Checked Exception的。對於編寫一個方法來講,“是否在方法上聲明一個異常”這個問題比“是否采用Checked Exception”更加重要。
總結
本文介紹了自己總結的Java異常處理的主要原理和原則,主要回答了這幾個主要的問題:1)Exception和Error的區別;2)聲明異常和未聲明異常的區別;3)什么情況下應拋出未聲明異常;4)理解如何捕獲和處理其他方法拋出的異常;5)自定義異常應繼承Exception還是RuntimeException。最后需要說的是,雖然有這些原理和原則可以指導,但是異常處理本質上還是一個需要根據具體情況仔細推敲的問題,這樣才能作出最合適的設計。