Source: http://www.cnblogs.com/everhad/p/5799974.html
異常概述
程序在運行中總會面臨一些“意外”情況,良好的代碼需要對它們進行預防和處理。大致來說,這些意外情況分三類:
- 交互輸入
用戶以非預期的方式使用程序,比如非法輸入,不正當的操作順序,輸入文件錯誤等。 - 軟件和硬件環境問題
文件不存在,文件格式錯誤,網絡問題,存儲空間不足,需要的預安裝庫不存在,系統版本不匹配等。 - 代碼錯誤
使用的其它代碼可能的執行錯誤,如調用了有關數學計算的方法中執行了除0操作等。
發現異常和處理異常都是困難的,需要非常嚴謹的代碼。實際上,程序總是分層或分模塊的,往往發生異常的地方和最終調用的地方“相距”甚遠。而且,異常的處理有時需要通知用戶,甚至需要用戶來決定接下來的動作。又或者,程序運行在“后臺”,對錯誤的只能是記錄措施。異常發生后,有的情況是需要從錯誤的狀態中恢復再繼續執行,又或者是保存狀態然后終止執行等。
有關異常的發現和預防是一個具體問題具體對待的經驗之談。對于異常處理框架,關鍵包括異常的表示、傳遞和捕獲。接下來我們結合Java提供的異常處理機制來學習下如何在正常的程序邏輯中加入異常處理的代碼。
Java中的異常處理機制
異常信息是為了通知更上層的方法調用者有關意外的情況,所以它有一個隨方法調用棧向上傳遞的過程,異常信息會像返回值那樣被層層上傳,直到有方法處理它。
作為面向對象語言,Java提供給我們的幾乎都是類、接口這些編程元素。異常處理也不例外,Java并不選擇使用返回值來表示異常信息(因為有時返回值無法表達異常情況,而且會搞亂正常的返回值含意,想象下返回任意int值的方法。你依然可以對返回值做很多約定,使用參數來攜帶異常信息也是受限的),而是定義了Throwable相關的類層次來表示異常。這樣可以保證正常代碼執行的簡明流程,而“異常發生”后將產生一個Throwable對象并隨著調用棧向上傳遞,對應方法立即退出,沒有任何返回值,調用方法的代碼收到異常后繼續退出并上傳到更上層方法調用,或者捕獲此異常。
接下來就依次來了解下Java異常框架提供的異常表示、傳遞和捕獲處理相關的實現細節。
異常表示:Throwable繼承層次
Java中的“異常”基類是Exception,表示可以被方法調用代碼處理的可恢復意外情形的“異常信息”。它又繼承自Throwable,Throwable是JVM可以throw(后面會講到)的所有類的基類,用來隨著方法調用棧向上傳遞表示產生此對象的方法的執行環境的信息,封裝了一個字符串描述以及方法的調用棧信息。它的另一個子類是Error,它只能由Java運行時本身錯誤時被創建,我們的app不要去繼承它,也無法處理它。
接下來所談及的異常都是Exception的子類,不涉及Error。
Throwable類提供了有關異常的文本描述和調用堆棧:
public String getMessage();
public StackTraceElement[] getStackTrace();
getMessage返回的方法主要是便于調試追蹤,如記錄日志或者給用戶看。而getStackTrace返回一個數組,StackTraceElement表示調用棧中一個調用的有關信息,如類名,方法名和語句的行號等。
Exception的子類有2個分支,RuntimeException是程序自身代碼邏輯引起的異常,如NullPointerException、IndexOutOfBoundsException,基本上可避免。其它異常類表示有關運行時不可避免的意外,例如程序輸入IOException、運行環境相關的非預期情況等。
從“含義”上去區分RuntimeException和非RuntimeException比較困難,另一個分類是,繼承自Error和RuntimeException的類都是未檢查(unchecked)異常,其它異常都是已檢查(checked)異常。所以對Exception子類而言,可以分為運行時異常
和已檢查異常
。它們的使用以及編譯器對待它們是不同的,后面會看到。
異常情形的表示盡量使用已有的“系統/框架”異常類,這樣很容易獲得“共識”。如果沒有合適的異常類,就可以設計自己的Exception子類(可以繼承某個已有異常類,或者設計自己的異常類層次,不過異常的層次不應該過于“深”,而應該保持扁平——更容易理解)。Exception類的代碼很簡短,這里直接給出:
/**
* {@code Exception} is the superclass of all classes that represent recoverable
* exceptions. When exceptions are thrown, they may be caught by application
* code.
*
* @see Throwable
* @see Error
* @see RuntimeException
*/
public class Exception extends Throwable {
private static final long serialVersionUID = -3387516993124229948L;
/**
* Constructs a new {@code Exception} that includes the current stack trace.
*/
public Exception() {
}
/**
* Constructs a new {@code Exception} with the current stack trace and the
* specified detail message.
*
* @param detailMessage
* the detail message for this exception.
*/
public Exception(String detailMessage) {
super(detailMessage);
}
/**
* Constructs a new {@code Exception} with the current stack trace, the
* specified detail message and the specified cause.
*
* @param detailMessage
* the detail message for this exception.
* @param throwable
* the cause of this exception.
*/
public Exception(String detailMessage, Throwable throwable) {
super(detailMessage, throwable);
}
/**
* Constructs a new {@code Exception} with the current stack trace and the
* specified cause.
*
* @param throwable
* the cause of this exception.
*/
public Exception(Throwable throwable) {
super(throwable);
}
}
根據規范,自己的異常類型需要提供一個無參構造器和一個接收String類型的異常描述信息參數的構造器。
class MyException extends Exception {
public MyException() {}
public MyException(String detailMessage) {
super(detailMessage);
}
}
detailMessage提供了有關異常的描述信息,可以調用getMessage()獲得它,對于調試非常有用。
上面的示例MyException繼承自Exception,這樣它就成為一個已檢查異常,相反地,如果MyException繼承自RuntimeException則它就成為一個未檢查異常。在深入探討異常的傳遞和捕獲之前,可以簡單地給出它們的區別:已檢查異常是用來表示那些運行中不可避免又不可預期的輸入、環境相關的異常,這些異常總是可能發生,因此必須顯示地處理它們。一個方法如果會產生已檢查異常,那么在通過編譯前,就必須在方法聲明部分一起使用throws關鍵字聲明將可能拋出這個異常,聲明意味著告訴調用方法在執行期間可能會拋出對應的異常對象。之后,調用者必須捕獲此異常,或繼續聲明拋出此異常,因此已檢查異常“顯式地”完成了異常的上傳,而且是編譯器的要求。未檢查異常則不需要顯示地去捕獲或聲明,只會在運行期間被拋出,然后隨調用棧上傳。
一般來說,自己的程序應該將代碼邏輯錯誤使用RuntimeException去表示,而涉及到輸入、環境等不可控的必然因素使用已檢查異常來表示。
異常的傳遞
知道如何表達異常信息后,接下來就是向上通知異常的發生。通知異常的方式就是使用throw關鍵字的語法“拋出”一個異常對象,過程是:
異常發生時,根據情況創建一個合適的異常類對象,因為異常類型是最終繼承自Throwable的,它創建后就從線程獲得了當前方法的調用棧信息。接著,可以為異常對象設置有關錯誤的描述,還可以增加額外字段攜帶必要的數據。最后執行throw語句:
MyException myEx = new MyException();
// ...設置異常信息
throw myEx;
拋出異常后,方法后續代碼不再執行,方法立即向上傳遞異常對象,或者說“發生了異常”。
為了分析異常傳遞的過程,下面制造一個若干方法之間形成鏈式調用的案例,它是一個控制臺程序:
public class ExceptionTest {
public static void main(String[] args) throws IOException {
new ExceptionTest().methodA();
}
public void methodA() throws IOException {
methodB();
}
public void methodB() throws IOException {
methodC();
}
public void methodC() throws IOException {
methodD();
if (System.currentTimeMillis() % 4 == 0) {
throw new IOException();
}
}
public void methodD() {
throw new RuntimeException();
}
}
如代碼所示,方法methodA的執行最終依次執行方法methodB、methodC、methodD。
運行方法methodA()將獲得如下的異常信息:
Exception in thread "main" java.lang.RuntimeException
at com.java.language.ExceptionTest.methodD(ExceptionTest.java:34)
at com.java.language.ExceptionTest.methodC(ExceptionTest.java:27)
at com.java.language.ExceptionTest.methodB(ExceptionTest.java:23)
at com.java.language.ExceptionTest.methodA(ExceptionTest.java:19)
at com.java.language.ExceptionTest.main(ExceptionTest.java:15)
methodD中產生異常,之后異常傳遞到調用methodA的main方法中,程序終止。
可能類似的打印信息我們見過不少次了,異常發生后方法調用棧的打印信息非常清晰地展示了此刻異常從methodD開始傳遞到main方法的經過的方法鏈。實際上任何時候都可以創建一個Throwable對象然后獲得當前的調用棧信息:
public static String getStackTraceMsg(Throwable tr) {
if (tr == null) {
return "";
}
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
tr.printStackTrace(pw);
pw.flush();
return sw.toString();
}
更詳細的調用棧信息可以通過Throwable類的public StackTraceElement[] getStackTrace()方法得到。
如果方法需要拋出已檢查異常,如methodC()中會拋出IOException,那么它必須在方法聲明中加入throws IOException語句,如果有多個已檢查異常則對于類型使用逗號隔開,類似implements實現多個接口那樣。
在了解如何捕獲異常之前,可以看到,RuntimeException會隨著方法調用棧依次上傳,直到到達最終調用者。而已檢查異常要求方法調用代碼在編譯前就聲明繼續拋出此異常(或者顯示地捕獲它)。
捕獲并處理異常
現在在合適的地方拋出了異常,并且默認地,異常會隨著方法調用棧依次向上傳遞,這樣,任何方法都可以在異常發生后獲得所調用的其它方法傳遞上來的異常對象了。
最后也是最重要的一步就是捕獲并處理異常了。
try/catch語法正是用來完成異常的捕獲的。在try塊中執行的語句,如果產生了異常,則catch塊會匹配該異常,如果產生的異常和catch捕獲的異常類型匹配——異常是catch捕獲的異常類型或者它的子類就判定為匹配——該異常就不再繼續上傳了,catch塊中可以獲得異常對象的信息,做相應的處理。
對于之前的案例,如果希望在方法methodB()中捕獲methodD()拋出的運行時異常可以這么做:
public void methodB() throws IOException {
try {
// 其它代碼
methodC();
} catch (RuntimeException e) {
// 這里處理異常.
}
}
再次運行上面的ExceptionTest.main方法,就不再出現異常打印信息了,因為運行時異常傳遞到 methodB()后被捕獲了。
try塊中發生異常后,try塊中后續代碼不再執行,接著會轉到匹配的catch塊中繼續執行,如果沒有任何匹配的catch則異常繼續向上層方法傳遞。try塊中的代碼沒有發生異常時,會正常執行所有語句,之后繼續執行try/catch塊后的其它代碼。
一個try塊可以對應多個catch,這是應對try中的語句可能產生多種不同類型異常的情況,此時的匹配規則是依次對各個catch塊執行匹配,一旦匹配就由該catch塊處理此異常。所以,在編寫多個catch時,注意它們之間的繼承結構所決定的不同的捕獲范圍:
try {
methodC();
} catch (Exception1 e1) {
// 處理異常1
} catch (Exception2 e2) {
// 處理異常2
} catch (Exception3 e3) {
// 處理異常3
} catch (Exception e) {
// 處理所有異常!!
}
注意catch塊的順序,避免前面的catch塊總是捕獲掉之后catch塊可捕獲的異常類型,這本身已經是邏輯錯誤了。
一個方法可以選擇使用try/catch來捕獲可能的運行時異常或已檢查異常,尤其對于調用了可拋出已檢查異常的方法時,必須顯示地去捕獲此異常,或者選擇繼續拋出這個已檢查異常。可以想象,聲明拋出已檢查異常,從某種含義上也是一種處理,實際上如果當前方法并沒有合適的處理方式時,就繼續拋出異常,而不去捕獲它。
finally塊
如果方法有一些代碼在異常發生與否時都需要一定執行到,可以為try/catch塊添加finally塊。注意finally塊需要放在最后,如果沒有catch塊的話直接就是try/finally的結構:
try {
// 一些語句,有可能拋出異常
} finally {
// 一定會執行到
}
finally塊中的代碼保證無論是否發生異常也會執行,雖然可以選擇在一個特別設計的catch中捕獲任何異常來完成同樣的目的,但是代碼會很丑陋,需要在try和catch中同時包含相應的代碼。
finally中的代碼是在“最后”執行的,當發生異常后,catch塊如果匹配,則對應的處理代碼會被執行,最后繼續執行完finally中的代碼。如果沒有異常發生,正常try中的代碼執行完畢后,依然繼續去執行finally中的代碼。
下面的方法有興趣可以試下,它實驗了finally語句中有關return語句的邏輯:
private static String funnyMethod(boolean runCatch, boolean runFinally) {
try {
if (runCatch) {
throw new RuntimeException();
} else {
return "normal return";
}
} catch (RuntimeException e) {
return "catch return";
} finally {
if (runFinally) {
return "finally return";
}
}
}
return關鍵字實際上做了兩件事情,設置方法的返回值,終止后續語句的執行。
在遇到一些資源必須被釋放這樣的情況時,就可以在finally中執行資源關閉這樣的操作。注意這些操作如果繼續產生異常的話,就try/catch執行它們。
更多要點
有關Java異常處理機制,還有很多細節上值得關注,下面是一個不完整的列表。
重寫方法時聲明已檢查異常
當一個子類重寫父類的方法時,它可以聲明的已檢查異常不能超出父類方法所聲明的那些。這樣,子類方法就需要顯式地捕獲語句中不可以拋出的已檢查異常。聲明的已檢查異常必須比父類方法中聲明的類型更具體化。
catch中再次拋出異常
catch塊中的代碼有可能再次拋出異常,所以有時需要在catch塊內部使用try/catch結構。另一些情況下,我們需要主動在catch塊在拋出異常。這有很多原因,例如當前方法的catch只是為了記錄下日志,之后希望原始的異常繼續傳遞。又或者自己的系統是分層或分模塊的,這時需要對調用者拋出更有描述意義的異常,可以重新在catch中拋出自己定義了的異常類型的對象。
Throwable提供了initCause方法用來對異常設置相應的原始異常,之后捕獲異常后調用getCause獲得原始異常:
/**
* Initializes the cause of this {@code Throwable}. The cause can only be
* initialized once.
*
* @param throwable
* the cause of this {@code Throwable}.
* @return this {@code Throwable} instance.
* @throws IllegalArgumentException
* if {@code Throwable} is this object.
* @throws IllegalStateException
* if the cause has already been initialized.
*/
public Throwable initCause(Throwable throwable);
/**
* Returns the cause of this {@code Throwable}, or {@code null} if there is
* no cause.
*/
public Throwable getCause();
所以,假設數據訪問層方法收到了SQLException,那么可以重新創建一個抽象的數據訪問的異常,把實際的SQLException設置為新異常的cause。
catch塊的異常參數
當出現多個catch塊時,catch(Exception ex)中的參數ex隱含為final變量,不可以對它賦值。
catch和finally中發生異常
catch和finally塊中都有可能繼續發生異常或主動拋出異常,這時如果try中已經有異常了,就會被覆蓋掉。一般的做法是繼續拋出try塊中本身的異常,然后使用Throwable.addSuppressed(Throwable throwable)方法把后面的異常添加到“原”異常的“壓制了的異常列表”中,調用者可以調用Throwable[] getSuppressed()方法獲得這些“伴隨”異常,如果需要的話。
避免不必要的異常
如果方法可以從約定上清晰的表達自己和調用者的各種使用規范,就不要去拋出異常。如果方法可以增加判斷來避免異常發生,就增加這些判斷。因為異常的產生會帶來性能問題,尤其是已檢查異常。
如果可以通過引用判斷來執行不同的流程,那么要比發生NullPointerException后傳遞給調用者具有更好的執行效率。只在真正的異常情形下去拋出異常。
不要盲目的壓制異常
很多人喜歡這樣的代碼:
try {
// 一些可能拋出各種各樣異常的語句
} catch (Exception ex) {
// 什么也不做,吃掉所有異常,好像什么也沒發生
}
過度捕獲異常很可能使得最終代碼的錯誤查找十分困難!數據和業務bug遠比語法層面的邏輯錯誤隱晦得多。
壓制不可能的異常
Java反射庫中的很多方法聲明了各種已檢查異常,在實際使用時也許基本上是100%肯定不會發生這些異常的,那么就大膽的壓制它們。否則隨著方法調用的傳遞,其它更多方法被動的聲明了那些完全不可能發生的異常。
早拋出,晚捕獲
早拋出:異常拋出的地方應該足夠及時,距離異常情形的原因最近的地方。所以方法一旦檢測到異常操作,就立即拋出異常來通知調用者,否則在更上層方法中發生其它異常可能更難理解。可以參考下Stack.pop、peek等方法的設計。
晚捕獲:異常的處理往往需要更上層的調用者才可以做出正確的決策,這時候框架中的方法就應該將異常傳遞出去,不要自己做任何不恰當的假設處理。尤其是那些和UI相關的操作。
異常類型的設計
盡量使用系統/框架已有的異常類型,減少沒必要的代碼溝通成本。
例外的情況是,自己的框架需要一套專有的異常繼承結構,主要是區分開其它框架的異常。自己的異常類型中可以增加額外的信息,如對異常來源的統一描述等,但框架內部方法沒必要舍棄合適的系統類型去增加重復概念。
參考資料
- Java核心技術 卷1 基礎知識 原書第9版
(本文使用Atom編寫)
![]() |
不含病毒。www.avast.com |