1引子
try…catch…finally恐怕是大家再熟悉不過的語句了,而且感覺用起來也是很簡單,邏輯上似乎也是很容易理解。不過,我親自體驗的“教訓”告訴我,這個東西可不是想象中的那麼簡單、聽話。不信?那你看看下面的代碼,“猜猜”它執行後的結果會是什嗎?不要往後看答案、也不許執行代碼看真正答案哦。如果你的答案是正確,那麼這篇文章你就不用浪費時間看啦。
package myExample.testException;
public class TestException {
public TestException() {
}
boolean testEx() throws Exception{
boolean ret = true;
try{
ret = testEx1();
}catch (Exception e){
System.out.println("testEx, catch exception");
ret = false;
throw e;
}finally{
System.out.println("testEx, finally; return value="+ret);
return ret;
}
}
boolean testEx1() throws Exception{
boolean ret = true;
try{
ret = testEx2();
if (!ret){
return false;
}
System.out.println("testEx1, at the end of try");
return ret;
}catch (Exception e){
System.out.println("testEx1, catch exception");
ret = false;
throw e;
}
finally{
System.out.println("testEx1, finally; return value="+ret);
return ret;
}
}
boolean testEx2() throws Exception{
boolean ret = true;
try{
int b=12;
int c;
for (int i=2;i>=-2;i--){
c=b/i;
System.out.println("i="+i);
}
return true;
}catch (Exception e){
System.out.println("testEx2, catch exception");
ret = false;
throw e;
}
finally{
System.out.println("testEx2, finally; return value="+ret);
return ret;
}
}
public static void main(String[] args) {
TestException testException1 = new TestException();
try{
testException1.testEx();
}catch(Exception e){
e.printStackTrace();
}
}
}
你的答案是什嗎?是下面的答案嗎?
i=2
i=1
testEx2, catch exception
testEx2, finally; return value=false
testEx1, catch exception
testEx1, finally; return value=false
testEx, catch exception
testEx, finally; return value=false
如果你的答案真的如上面所說,那麼你錯啦。^_^,那就建議你仔細看一看這篇文章或者拿上面的代碼按各種不同的情況修改、執行、測試,你會發現有很多事情不是原來想象中的那麼簡單的。
現在公布正確答案:
i=2
i=1
testEx2, catch exception
testEx2, finally; return value=false
testEx1, finally; return value=false
testEx, finally; return value=false
2基礎知識
2.1 相關概念
例外是在程式運行過程中發生的例外狀況事件,比如除0溢出、數組越界、檔案找不到等,這些事件的發生將阻止程式的正常運行。為了加強程式的魯棒性,程式設計時,必須考慮到可能發生的例外狀況事件並做出相應的處理。C語言中,通過使用if語句來判斷是否出現了例外,同時,調用函數通過被調用函數的傳回值感知在被調用函數中產生的例外事件並進行處理。全程變數ErroNo常常用來反映一個例外狀況事件的類型。但是,這種錯誤處理機制會導致不少問題。
Java通過物件導向的方法來處理例外。在一個方法的運行過程中,如果發生了例外,則這個方法產生代表該例外的一個對象,並把它交給運行時系統,運行時系統尋找相應的代碼來處理這一例外。我們把產生例外對象並把它提交給運行時系統的過程稱為拋棄(throw)一個例外。運行時系統在方法的調用棧中尋找,從產生例外的方法開始進行回朔,直到找到包含相應例外處理的方法為止,這一個過程稱為捕獲(catch)一個例外。
2.2Throwable類及其子類
用物件導向的方法處理例外,就必須建立類的層次。類 Throwable位於這一類層次的最頂層,只有它的後代才可以做為一個例外被拋棄。圖1表示了例外處理的類層次。
可以看出,類Throwable有兩個直接子類:Error和Exception。Error類對象(如動態串連錯誤等),由Java虛擬機器產生並拋棄(通常,Java程式不對這類例外進行處理);Exception類對象是Java程式處理或拋棄的對象。它有各種不同的子類分別對應於不同類型的例外。其中類RuntimeException代表運行時由Java虛擬機器產生的例外,如算術運算例外ArithmeticException(由除0錯等導致)、數組越界例外ArrayIndexOutOfBoundsException等;其它則為非運行時例外,如輸入輸出例外IOException等。Java編譯器要求Java程式必須捕獲或聲明所有的非運行時例外,但對運行時例外可以不做處理。
圖1 例外處理的類層次
2.3 異常處理關鍵字
Java的異常處理是通過5個關鍵字來實現的:try,catch,throw,throws,finally。JB的線上協助中對這幾個關鍵字是這樣解釋的:
Throws: Lists the exceptions a method could throw.
Throw: Transfers control of the method to the exception handler.
Try: Opening exception-handling statement.
Catch: Captures the exception.
Finally: Runs its code before terminating the program.
2.3.1try語句
try語句用大括弧{}指定了一段代碼,該段代碼可能會拋棄一個或多個例外。
2.3.2catch語句
catch語句的參數類似於方法的聲明,包括一個例外類型和一個例外對象。例外類型必須為Throwable類的子類,它指明了catch語句所處理的例外類型,例外對象則由運行時系統在try所指定的代碼塊中產生並被捕獲,大括弧中包含對象的處理,其中可以調用對象的方法。
catch語句可以有多個,分別處理不同類的例外。Java運行時系統從上到下分別對每個catch語句處理的例外類型進行檢測,直到找到類型相匹配的catch語句為止。這裡,類型匹配指catch所處理的例外類型與產生的例外對象的類型完全一致或者是它的父類,因此,catch語句的排列順序應該是從特殊到一般。
也可以用一個catch語句處理多個例外類型,這時它的例外型別參數應該是這多個例外類型的父類,程式設計中要根據具體的情況來選擇catch語句的例外處理類型。
2.3.3 finally語句
try所限定的代碼中,當拋棄一個例外時,其後的代碼不會被執行。通過finally語句可以指定一塊代碼。無論try所指定的程式塊中拋棄或不拋棄例外,也無論catch語句的例外類型是否與所拋棄的例外的類型一致,finally所指定的代碼都要被執行,它提供了統一的出口。通常在finally語句中可以進行資源的清除工作。如關閉開啟的檔案等。
2.3.4 throws語句
throws總是出現在一個函數頭中,用來標明該成員函數可能拋出的各種異常。對大多數Exception子類來說,Java 編譯器會強迫你聲明在一個成員函數中拋出的異常的類型。如果異常的類型是Error或 RuntimeException, 或它們的子類,這個規則不起作用,因為這在程式的正常部分中是不期待出現的。如果你想明確地拋出一個RuntimeException,你必須用throws語句來聲明它的類型。
2.3.5throw語句
throw總是出現在函數體中,用來拋出一個異常。程式會在throw語句後立即終止,它後面的語句執行不到,然後在包含它的所有try塊中(可能在上層調用函數中)從裡向外尋找含有與其匹配的catch子句的try塊。
3 關鍵字及其中語句流程詳解
3.1try的嵌套
你可以在一個成員函數調用的外面寫一個try語句,在這個成員函數內部,寫另一個try語句保護其他代碼。每當遇到一個try語句,異常的架構就放到堆棧上面,直到所有的try語句都完成。如果下一級的try語句沒有對某種異常進行處理,堆棧就會展開,直到遇到有處理這種異常的try語句。下面是一個try語句嵌套的例子。
class MultiNest {
static void procedure() {
try {
int a = 0;
int b = 42/a;
} catch(java.lang.ArithmeticException e) {
System.out.println("in procedure, catch ArithmeticException: " + e);
}
}
public static void main(String args[]) {
try {
procedure();
} catch(java.lang. Exception e) {
System.out.println("in main, catch Exception: " + e);
}
}
}
這個例子執行的結果為:
in procedure, catch ArithmeticException: java.lang.ArithmeticException: / by zero
成員函數procedure裡有自己的try/catch控制,所以main不用去處理 ArrayIndexOutOfBoundsException;當然如果如同最開始我們做測試的例子一樣,在procedure中catch到異常時使用throw e;語句將異常拋出,那麼main當然還是能夠捕捉並處理這個procedure拋出來的異常。例如在procedure函數的catch中的System.out語句後面增加throw e;語句之後,執行結果就變為:
in procedure, catch ArithmeticException: java.lang.ArithmeticException: / by zero
in main, catch Exception: java.lang.ArithmeticException: / by zero
3.2 try-catch程式塊的執行流程以及執行結果
相對於try-catch-finally程式塊而言,try-catch的執行流程以及執行結果還是比較簡單的。
首先執行的是try語句塊中的語句,這時可能會有以下三種情況:
1. 如果try塊中所有語句正常執行完畢,那麼就不會有其他的“動做”被執行,整個try-catch程式塊正常完成。
2.如果try語句塊在執行過程中碰到異常V,這時又分為兩種情況進行處理:
如果異常V能夠被與try相應的catch塊catch到,那麼第一個catch到這個異常的catch塊(也是離try最近的一個與異常V匹配的catch塊)將被執行;如果catch塊執行正常,那麼try-catch程式塊的結果就是“正常完成”;如果該catch塊由於原因R突然中止,那麼try-catch程式塊的結果就是“由於原因R突然中止(completes abruptly)”。
如果異常V沒有catch塊與之匹配,那麼這個try-catch程式塊的結果就是“由於拋出異常V而突然中止(completes abruptly)”。
3.如果try由於其他原因R突然中止(completes abruptly),那麼這個try-catch程式塊的結果就是“由於原因R突然中止(completes abruptly)”。
3.3 try-catch-finally程式塊的執行流程以及執行結果
try-catch-finally程式塊的執行流程以及執行結果比較複雜。
首先執行的是try語句塊中的語句,這時可能會有以下三種情況:
1.如果try塊中所有語句正常執行完畢,那麼finally塊的居於就會被執行,這時分為以下兩種情況:
如果finally塊執行順利,那麼整個try-catch-finally程式塊正常完成。
如果finally塊由於原因R突然中止,那麼try-catch-finally程式塊的結局是“由於原因R突然中止(completes abruptly)”
2.如果try語句塊在執行過程中碰到異常V,這時又分為兩種情況進行處理:
如果異常V能夠被與try相應的catch塊catch到,那麼第一個catch到這個異常的catch塊(也是離try最近的一個與異常V匹配的catch塊)將被執行;這時就會有兩種執行結果:
如果catch塊執行正常,那麼finally塊將會被執行,這時分為兩種情況:
如果finally塊執行順利,那麼整個try-catch-finally程式塊正常完成。
如果finally塊由於原因R突然中止,那麼try-catch-finally程式塊的結局是“由於原因R突然中止(completes abruptly)”
如果catch塊由於原因R突然中止,那麼finally模組將被執行,分為兩種情況:
如果如果finally塊執行順利,那麼整個try-catch-finally程式塊的結局是“由於原因R突然中止(completes abruptly)”。
如果finally塊由於原因S突然中止,那麼整個try-catch-finally程式塊的結局是“由於原因S突然中止(completes abruptly)”,原因R將被拋棄。
(注意,這裡就正好和我們的例子相符合,雖然我們在testEx2中使用throw e拋出了異常,但是由於testEx2中有finally塊,而finally塊的執行結果是complete abruptly的(別小看這個用得最多的return,它也是一種導致complete abruptly的原因之一啊——後文中有關於導致complete abruptly的原因分析),所以整個try-catch-finally程式塊的結果是“complete abruptly”,所以在testEx1中調用testEx2時是捕捉不到testEx1中拋出的那個異常的,而只能將finally中的return結果擷取到。
如果在你的代碼中期望通過捕捉被調用的下級函數的異常來給定傳回值,那麼一定要注意你所調用的下級函數中的finally語句,它有可能會使你throw出來的異常並不能真正被上級調用函數可見的。當然這種情況是可以避免的,以testEx2為例:如果你一定要使用finally而且又要將catch中throw的e在testEx1中被捕獲到,那麼你去掉testEx2中的finally中的return就可以了。
這個事情已經在OMC2.0的MIB中出現過啦:伺服器的異常不能完全被反饋到用戶端。)
如果異常V沒有catch塊與之匹配,那麼finally模組將被執行,分為兩種情況:
如果finally塊執行順利,那麼整個try-catch-finally程式塊的結局就是“由於拋出異常V而突然中止(completes abruptly)”。
如果finally塊由於原因S突然中止,那麼整個try-catch-finally程式塊的結局是“由於原因S突然中止(completes abruptly)”,異常V將被拋棄。
如果try由於其他原因R突然中止(completes abruptly),那麼finally塊被執行,分為兩種情況:
如果finally塊執行順利,那麼整個try-catch-finally程式塊的結局是“由於原因R突然中止(completes abruptly)”。
如果finally塊由於原因S突然中止,那麼整個try-catch-finally程式塊的結局是“由於原因S突然中止(completes abruptly)”,原因R將被拋棄。
3.4 try-catch-finally程式塊中的return
從上面的try-catch-finally程式塊的執行流程以及執行結果一節中可以看出無論try或catch中發生了什麼情況,finally都是會被執行的,那麼寫在try或者catch中的return語句也就不會真正的從該函數中跳出了,它的作用在這種情況下就變成了將控制權(語句流程)轉到finally塊中;這種情況下一定要注意傳回值的處理。
例如,在try或者catch中return false了,而在finally中又return true,那麼這種情況下不要期待你的try或者catch中的return false的傳回值false被上級調用函數擷取到,上級調用函數能夠擷取到的只是finally中的傳回值,因為try或者catch中的return語句只是轉移控制權的作用。
3.5 異常的分類
1.1
異常的繼承結構:基類為Throwable,Error和Exception繼承Throwable,RuntimeException和IOException等繼承Exception,具體的RuntimeException繼承RuntimeException。
1.2
Error和RuntimeException及其子類成為未檢查異常(unchecked),其它異常成為已檢查異常(checked)。
2 每個類型的異常的特點
2.1
Error體系
Error類體系描述了Java運行系統中的內部錯誤以及資源耗盡的情形。應用程式不應該拋出這種類型的對象(一般是由虛擬機器拋出)。如果出現這種錯誤,除了儘力使程式安全退出外,在其他方面是無能為力的。所以,在進行程式設計時,應該更關注Exception體系。
2.2 Exception體系
Exception體系包括RuntimeException體系和其他非RuntimeException的體系
2.2.1
RuntimeException
RuntimeException體系包括錯誤的類型轉換、數組越界訪問和試圖訪問null 指標等等。處理RuntimeException的原則是:如果出現RuntimeException,那麼一定是程式員的錯誤。例如,可以通過檢查數組下標和數組邊界來避免數組越界訪問異常。
2.2.2
其他(IOException等等)
這類異常一般是外部錯誤,例如試圖從檔案尾後讀取資料等,這並不是程式本身的錯誤,而是在應用環境中出現的外部錯誤。