異常指的是程序運行時出現(xiàn)的不正常情況。程序運行過程中難免會發(fā)生異常,發(fā)生異常并不可怕,程序員應(yīng)該考慮到有可能發(fā)生這些異常,編程時應(yīng)能正確的處理異常,使成為健壯的程序。
異常是相對于 return 的一種退出機制,可以由系統(tǒng)觸發(fā),也可以由程序通過 throw 語句觸發(fā),異??梢酝ㄟ^ try/catch 語句進行捕獲并處理,如果沒有捕獲,則會導(dǎo)致程序退出并輸出異常棧信息。
異常的層次
Java 的異常類是處理運行時的特殊類,每一種異常對應(yīng)一種特定的運行錯誤.所有Java異常類都是系統(tǒng)類庫中 Exception 類的子類。

Throwable 類
所有的異常類都直接或間接地繼承于 java.lang.Throwable 類,在 Throwable 類有幾個非常重要的方法:
- String getMessage():獲得發(fā)生異常的詳細(xì)消息。
- void printStackTrace():打印異常堆棧跟蹤信息。
- void printStackTrace(PrintStream s) 通常用該方法將異常內(nèi)容保存在日志文件中,以便查閱。
- String toString():獲得獲取異常類名和異常信息的描述。
public class Throwable implements Serializable {
...
public String getMessage() {
return detailMessage;
}
public void printStackTrace() {
printStackTrace(System.err);
}
public void printStackTrace(PrintStream s) {
printStackTrace(new WrappedPrintStream(s));
}
public String toString() {
String s = getClass().getName();
String message = getLocalizedMessage();
return (message != null) ? (s + ": " + message) : s;
}
...
}
Error 和 Exception
Throwable 有兩個直接子類:Error 和 Exception。
Error 是程序無法恢復(fù)的嚴(yán)重錯誤,程序員根本無能為力,程序中不能對其編程處理, 對 Error 一般不編寫針對性的代碼對其進行處理 只能讓程序終止。例如:JVM 內(nèi)部錯誤、內(nèi)存溢出和資源耗盡等嚴(yán)重情況。
Exception 是程序可以恢復(fù)的異常,它是程序員所能掌控的。例如:除零異常、空指針訪問、網(wǎng)絡(luò)連接中斷和讀取不存在的文件等。
受檢查異常和運行時異常
Java 的異常處理機制會區(qū)分兩種不同的異常類型:已檢異常 checked 和未檢異常 unchecked。
已檢異常
在明確的特定情況下拋出,經(jīng)常是應(yīng)用能部分或完全恢復(fù)的情況。例如,某段代碼要在多個可能的目錄中尋找配置文件。如果試圖打開的文件不在某個目錄中,就會拋出 FileNotFoundException 異常。在這個例子中,我們想捕獲這個異常,然后在文件可能出現(xiàn)的下一個位置繼續(xù)嘗試。也就是說,雖然文件不存在是異常狀況,但可以從中恢復(fù),這是意料之中的失敗。
非受檢異常
在 Java 環(huán)境中有些失敗是無法預(yù)料的,這些失敗可能是由運行時條件或濫用庫代碼導(dǎo)致的。例如把無效的 null 傳給使用對象或數(shù)組的方法,會拋出 NullPointerException 異常?;旧先魏畏椒ㄔ谌魏螘r候都可能拋出未檢異常。這是 Java 環(huán)境中的墨菲定律:“會出錯的事總會出錯?!睆奈礄z異常中恢復(fù),雖說不是不可能,但往往很難,因為完全不可預(yù)知。運行時異常往往是程序員所犯錯誤導(dǎo)致的,健壯的程序不應(yīng)該發(fā)生運行時異常。
若想?yún)^(qū)分已檢異常和未檢異常,記住兩點:異常是 Throwable 對象,而且異常主要分為兩類,通過 Error 和 Exception 子類標(biāo)識。只要異常對象是 Error 類,就是未檢異常。Exception 類還有一個子類 RuntimeException , RuntimeException 類的所有子類都屬于未檢異常。除此之外,都是已檢異常。
提示:對于運行時異常通常不采用拋出或捕獲處理方式,而是應(yīng)該提前預(yù)判,防止這種發(fā)生異常,做到未雨綢繆。例如在進行除法運算之前應(yīng)該判斷除數(shù)是非零的,修改代碼進行提前預(yù)判這樣處理要比通過 try-catch 捕獲異常要友好的多。
對比受檢和未受檢異常
通過以上介紹可以看出,未受檢異常和受檢異常的區(qū)別如下:受檢異常必須出現(xiàn)在 throws 語句中,調(diào)用者必須處理,Java 編譯器會強制這一點,而未受檢異常則沒有這個要求。
為什么要有這個區(qū)分呢?我們自己定義異常的時候應(yīng)該使用受檢還是未受檢異常呢?對于這個問題,業(yè)界有各種各樣的觀點和爭論,沒有特別一致的結(jié)論。
一種普遍的說法是:未受檢異常表示編程的邏輯錯誤,編程時應(yīng)該檢查以避免這些錯誤,比如空指針異常,如果真的出現(xiàn)了這些異常,程序退出也是正常的,程序員應(yīng)該檢查程序代碼的 bug 而不是想辦法處理這種異常。受檢異常表示程序本身沒問題,但由于 I/O、網(wǎng)絡(luò)、數(shù)據(jù)庫等其他不可預(yù)測的錯誤導(dǎo)致的異常,調(diào)用者應(yīng)該進行適當(dāng)處理。
但其實編程錯誤也是應(yīng)該進行處理的,尤其是 Java 被廣泛應(yīng)用于服務(wù)器程序中,不能因為一個邏輯錯誤就使程序退出。所以,目前一種更被認(rèn)同的觀點是:Java 中對受檢異常和未受檢異常的區(qū)分是沒有太大意義的,可以統(tǒng)一使用未受檢異常來代替。
這種觀點的基本理由是:無論是受檢異常還是未受檢異常,無論是否出現(xiàn)在 throws 聲明中,都應(yīng)該在合適的地方以適當(dāng)?shù)姆绞竭M行處理,而不只是為了滿足編譯器的要求盲目處理異常,既然都要進行處理異常,受檢異常的強制聲明和處理就顯得煩瑣,尤其是在調(diào)用層次比較深的情況下。
其實觀點本身并不太重要,更重要的是一致性,一個項目中,應(yīng)該對如何使用異常達成一致,并按照約定使用。
常見異常
Exception 類有若干子類,每個子類代表一種特定的運行錯誤,這些子類有的是系統(tǒng)事先定義好并包含在 Java 類庫中的,成為系統(tǒng)定義的運行異常。
- ClassNotFoundException 未找到要裝載的類
- ArrayIndexOutOfBoundsException 數(shù)組越界訪問
- FileNotFoundException 文件找不到, checked異常
- IOException 輸入, 輸出錯誤, checked 異常
- NullPointerException 空指針異常, unchecked 異常
- ArithmeticException 算術(shù)運算錯誤
- InterruptedException 中斷異常, 線程在進行暫停處理時(如睡眠)被調(diào)度打斷將引發(fā)該異常
異常的處理
- 對待受檢查異常。如果當(dāng)前方法有能力解決,則捕獲異常進行處理;沒有能力解決,則拋出給上層調(diào)用方法處理。
- 涉及了五個關(guān)鍵字
try catch finally throw throws。 - try...catch..finally 或者 try-with-resources(Java7增加)語句結(jié)構(gòu)進行捕獲
- try 必須帶有 catch 或者 finally,兩者至少二選一或者資源聲明才可以使用。
- 一個 try 可以引導(dǎo)多個 catch 塊。但是不要定義多余的 catch 塊,多個 catch 塊的異常出現(xiàn)繼承關(guān)系,父類異常 catch 塊放在最后面。
- 異常發(fā)生后,try 塊中的剩余語句將不再執(zhí)行。
- catch 塊中的代碼要執(zhí)行的條件是,首先在 try 塊中發(fā)生了異常,其次異常的類型與 catch 要捕捉的一致。 建議聲明更為具體的異常,這樣處理的可以更具體。當(dāng)捕獲的多個異常類之間存在父子關(guān)系時,捕獲異常順序與 catch 代碼塊的順序有關(guān)。一般先捕獲子類,后捕獲父類,否則子類捕獲不到。
- 可以無 finally 部分,但如果存在,則無論異常發(fā)生否,finally 部分的語句均要執(zhí)行。即便是 try 或 catch 塊中含有退出方法的語句 return,也不能阻止 finally 代碼塊的執(zhí)行; 除非執(zhí)行 System.exit(0) 等導(dǎo)致程序停止運行的語句。
try-catch 不僅可以嵌套在 try 代碼塊中,還可以嵌套在 catch 代碼塊或 finally 代碼塊,finally 代碼塊后面會詳細(xì)介紹。try-catch 嵌套會使程序流程變的復(fù)雜,如果能用多catch捕獲的異常,盡量不要使用 try catch 嵌套。特別對于初學(xué)者不要簡單地使用 Eclipse 的語法提示不加區(qū)分地添加 try-catch 嵌套,要梳理好程序的流程再考慮 try-catch 嵌套的必要性。
傳統(tǒng)處理
try {語句塊;}
catch (異常類名 參變量名) {語句塊;}
finally {語句塊;} // 定義一定執(zhí)行的代碼:通常用于關(guān)閉資源
Java 7 推出了多重捕獲(multi-catch)技術(shù), 可以把這些異常合并處理
try {
// 可能會發(fā)生異常的語句
} catch (IOException | ParseException e) {
// 調(diào)用方法 methodA 處理
}
finally 代碼塊
try-catch 語句后面還可以跟有一個 finally 代碼塊,try-catch-finally 語句語法如下:
注意:為了代碼簡潔等目的,可能有的人會將 finally 代碼中的多個嵌套的try-catch 語句合并。每一個close()方法對應(yīng)關(guān)閉一個資源,如果某一個 close() 方法關(guān)閉時發(fā)生了異常,那么后面的也不會關(guān)閉,因此這種代碼是有缺陷的。
釋放資源
有時在 try-catch 語句中會占用一些非 Java 資源,如:打開文件、網(wǎng)絡(luò)連接、打開數(shù)據(jù)庫連接和使用數(shù)據(jù)結(jié)果集等,這些資源并非 Java 資源,不能通過 JVM 的垃圾收集器回收,需要程序員釋放。為了確保這些資源能夠被釋放可以使用 finally 代碼塊或 Java 7 之后提供自動資源管理(Automatic Resource Management)技術(shù)。
自動資源管理
使用 finally 代碼塊釋放資源會導(dǎo)致程序代碼大量增加,一個 finally 代碼塊往往比正常執(zhí)行的程序還要多。在Java 7之后提供自動資源管理(Automatic Resource Management)技術(shù),可以替代 finally 代碼塊,優(yōu)化代碼結(jié)構(gòu),提高程序可讀性。
自動資源管理是在 try 語句上的擴展,語法如下:
try (聲明或初始化資源語句) {
//可能會生成異常語句
} catch(Throwable e1){
//處理異常e1
} catch(Throwable e2){
//處理異常e1
} catch(Throwable eN){
//處理異常eN
}
在 try 語句后面添加一對小括號“()”,其中是聲明或初始化資源語句,可以有多條語句語句之間用分號“;”分隔。
示例代碼如下:
public class HelloWorld {
public static void main(String[] args) {
Date date = readDate();
System.out.println("讀取的日期 = " + date);
}
public static Date readDate() {
// 自動資源管理
try (FileInputStream readfile = new FileInputStream("readme.txt");
InputStreamReader ir = new InputStreamReader(readfile);
BufferedReader in = new BufferedReader(ir)) {
// 讀取文件中的一行數(shù)據(jù)
String str = in.readLine();
if (str == null) return null;
DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
Date date = df.parse(str);
return date;
} catch (IOException e) {
System.out.println("處理IOException...");
e.printStackTrace();
} catch (ParseException e) {
System.out.println("處理ParseException...");
e.printStackTrace();
}
return null;
}
}
注意 所有可以自動管理的資源需要實現(xiàn) AutoCloseable 接口,上述代碼中三個輸入流 FileInputStream、InputStreamReader 和 BufferedReader 從 Java 7之后實現(xiàn) AutoCloseable 接口,具體哪些資源實現(xiàn) AutoCloseable 接口需要查詢 API文檔。
資源的聲明和初始化放在 try 語句內(nèi),不用再調(diào)用 finally,在語句執(zhí)行完 try語句后,會自動調(diào)用資源的 close() 方法。資源可以定義多個,以分號分隔。在 Java 9 之前,資源必須聲明和初始化在 try 語句塊內(nèi),Java 9去除了這個限制,資源可以在try語句外被聲明和初始化,但必須是 final 的或者是事實上 final 的(即雖然沒有聲明為final但也沒有被重新賦值)。
finally 語句有一個執(zhí)行細(xì)節(jié),如果在 try 或者 catch 語句內(nèi)有 return 語句,則 return 語句在 finally 語句執(zhí)行結(jié)束后才執(zhí)行,但 finally 并不能改變返回值,我們來看下面的代碼。
public static int yyy() {
int ret = 1;
try {
return ret;
} finally {
ret = 2;
}
}
輸出結(jié)果為 1。
如果在 finally 中也有 return 語句呢?try 和 catch 內(nèi)的 return 會丟失,實際會返回 finally 中的返回值。
如果在 finally 中也有 return 語句呢?try 和 catch 內(nèi)的 return 會丟失,實際會返回 finally 中的返回值。所以,一般而言,為避免混淆,應(yīng)該避免在 finally中使用 return 語句或者拋出異常,如果調(diào)用的其他代碼可能拋出異常,則應(yīng)該捕獲異常并進行處理。
throws 與聲明方法拋出異常
在一個方法中如果能夠處理異常,則需要捕獲并處理。但是本方法沒有能力處理該異常,捕獲它沒有任何意義,則需要在方法后面聲明拋出該異常,通知上層調(diào)用者該方法有可以發(fā)生異常。
注意:如果聲明拋出的多個異常類之間有父子關(guān)系,可以只聲明拋出父類。但如果沒有父子關(guān)系情況下,最好明確聲明拋出每一個異常,因為上層調(diào)用者會根據(jù)這些異常信息進行相應(yīng)的處理。假如一個方法中有可能拋出 IOException 和 ParseException 兩個異常,那么聲明拋出 IOException 和 ParseException 呢?還是只聲明拋出 Exception 呢?因為 Exception 是 IOException 和 ParseException 的父類,只聲明拋出 Exception 從語法是允許的,但是聲明拋出 IOException 和 ParseException 更好一些。
- 使用 throw 拋出異常. 異常的本質(zhì)是對象因為 throw 關(guān)鍵詞后跟的是 new 運算符來創(chuàng)建的一個異常對象。
- 使用 throws 關(guān)鍵字拋出一個或多個異常。
自定義異常
有些公司為了提高代碼的可重用性,自己開發(fā)了一些 Java 類庫或框架,其中少不了自己編寫了一些異常類。實現(xiàn)自定義異常類需要繼承 Exception 類或其子類,如果自定義運行時異常類需繼承 RuntimeException 類或其子類。
我們通過繼承 Exception 或者 RuntimeException 來定義一個異常。
Java 內(nèi)部定義 WebServiceException 示例:
package javax.xml.ws;
public class WebServiceException extends java.lang.RuntimeException {
public WebServiceException() {
super();
}
public WebServiceException(String message) {
super(message);
}
public WebServiceException(String message, Throwable cause) {
super(message,cause);
}
public WebServiceException(Throwable cause) {
super(cause);
}
}
和很多其他異常類一樣,我們沒有定義額外的屬性和代碼,只是繼承了Exception,定義了構(gòu)造方法并調(diào)用了父類的構(gòu)造方法。
throw 與顯式拋出異常
通過 throw 語句顯式拋出異常, 顯式拋出異常目的有很多,例如不想某些異常傳給上層調(diào)用者,可以捕獲之后重新顯式拋出另外一種異常給調(diào)用者。
注意:throw 顯式拋出的異常與系統(tǒng)生成并拋出的異常,在處理方式上沒有區(qū)別,就是兩種方法:要么捕獲自己處理,要么拋出給上層調(diào)用者。
設(shè)計良好異常機制
- 考慮要在異常中存儲什么額外狀態(tài)——記住,異常也是對象;
- Exception 類有四個公開的構(gòu)造方法,一般情況下,自定義異常類時這四個構(gòu)造方法都要實現(xiàn),可用于初始化額外的狀態(tài),或者定制異常消息;
- 不要在你的 API 中自定義很多細(xì)致的異常類——Java I/O 和反射 API 都因為這么做了而受人詬病,所以別讓使用這些包時的情況變得更糟;
- 別在一個異常類型中描述太多狀況——例如,實現(xiàn) JavaScript 的 Nashorn 引擎(Java 8 新功能)一開始有超多粗制濫造的異常,不過在發(fā)布之前修正了。
異常在子類覆蓋中的體現(xiàn)
- 子類覆蓋父類時, 如果父類的方法拋出的異常,那么子類只能拋出父類異?;蛟摦惓5淖宇?/strong>.
- 如果父類方法拋出多個異常, 那么子類在覆蓋方法時,只能拋出父類異常的子集.
- 如果父類或接口的方法中沒有異常拋出, 那么子類在覆蓋方法時,也不可能拋出異常.如果子類方法發(fā)生異常,就必須進行 try 處理,絕對不能拋.
一句話就是父類限制了子類可拋出的異常。
有爭議的兩種處理異常的反模式
// 不要捕獲異常而不處理
try {
someMethodThatMightThrow();
} catch(Exception e){
}
// 不要捕獲,記錄日志后再重新拋出異常
try {
someMethodThatMightThrow();
} catch(SpecificException e){
log(e);
throw e;
}
第一個反模式直接忽略近乎一定需要處理的異常狀況(甚至沒有在日志中記錄)。這么做會增大系統(tǒng)其他地方出現(xiàn)問題的可能性——出現(xiàn)問題的地方可能會離原來的位置很遠(yuǎn)。除非真的確定即使爆出異常,則可以忽略。
第二個反模式只會增加干擾——雖然記錄了錯誤消息,但沒真正處理發(fā)生的問題——在系統(tǒng)高層的某部分代碼中還是要處理這個問題。一般的操作是在catch 塊中比如進行資源關(guān)閉的工作,然后可重新拋出異常 throw e。
參考
第 14 章 異常處理-圖靈社區(qū)
http://www.ituring.com.cn/book/tupubarticle/17745Java 編程的邏輯-微信讀書
https://weread.qq.com/web/reader/b51320f05e159eb51b29226kc81322c012c81e728d9d180