導語: 代碼運行時,總會出現錯誤或者是異常。因此,Java從C++繼承了以面向對象方式處理異常的機制,用對象的方式來表示一個(或一類)異常,從而使開發人員寫出具有容錯性的健壯的代碼。
異常概述
Java異常體系結構
首先介紹一下Java異常體系結構,如下圖所示:
在Java中,任何異常都是Throwable
類或其子類對象;Throwable有兩個子類,分別是:Error
和Exception
;
Error
:這是系統錯誤類,是程序運行時Java內部的錯誤,一般是由硬件或操作系統引起的,開發人員一般無法處理,這類異常發生時,只能關閉程序。
Exception
:這是異常類,該類及其子類對象表示的錯誤一般是由算法考慮不周或編碼時疏忽所致,需要開發人員處理。
Java異常分類
Java中的異常被分爲兩大類:Runtime異常
和非Runtime異常
,其中非Runtime異常也被稱作Checked異常
。
Runtime異常
:是Java程序運行時產生的異常,例如:數組下標越界、空指針異常、對象類型強制轉換錯誤等。Error
和RuntimeException
都是Runtime異常,編譯時對這類異常不做檢查。
非Runtime異常
:也稱Checked異常
,例如IOException等。在編譯時,編譯器會對這類異常進行檢查,看看有沒有對這類異常進行處理,如果沒有進行處理,編譯會無法通過。
異常處理機制
異常的捕獲與處理
Java採用try-catch-finally語句來對異常進行捕獲並處理。該語句的語法格式如下:
try{
//可能會產生異常的代碼
}
catch(Exception_1 e1){
//處理Exception_1的代碼
}
catch(Exception_2 e2){
//處理Exception_2的代碼
}
……
catch(Exception_n en){
//處理Exception_n的代碼
}
finally{
//通常是釋放資源的代碼
}
其中catch和finally都是可以默認的(即可以不寫),但是不允許同時默認catch和finally。
try語句塊
:該語句塊中是程序正常情況下應該要完成的功能,而這些代碼中可能會產生異常,其後面的catch語句塊就是用來捕獲並處理這些異常的。
catch語句塊
:該語句塊用來捕獲並處理try語句塊中產生的異常。每個catch語句塊聲明其能處理的一種特定類型的異常,catch後面的括號中就是該特定類型的異常;不過,在Java7以前,每個catch語句塊只能捕獲一種異常,但是,從Java7開始就支持一個catch捕獲多種異常,多個異常之間用|
隔開。寫法如下:
try{
//可能會產生異常的代碼
}
catch(Exception_1 | Exception_2 | Exception_3 | ... | Exception_n e1){
//統一處理Exception_1、Exception_2、Exception_3、...、Exception_n的代碼
}
finally{
//通常是釋放資源的代碼
}
catch語句塊中聲明的異常對象(catch(Exception_n e)
)封裝了該異常的信息,可以通過該異常對象的一些方法來獲取這些信息:
String getMessage()
:用來獲取有關異常事件的詳細信息。
void printStackTrace()
:用來跟蹤異常事件發生時執行堆棧的內容。
finally語句塊
:該語句塊一般用於釋放資源等操作。無論try語句塊中是否有異常,finally語句塊都會執行。
try-catch-finally執行流程(最基本的情況)
這裏先介紹一下try-catch-finally執行流程最基本的情況,所謂最基本的情況就是沒有用到throw關鍵字(拋出異常,後面會講到)、return關鍵字等。
首先,執行try中的代碼,如果 try中的代碼沒有發生異常,那麼catch中的代碼就不執行,等try中的代碼執行完畢後直接執行finally中的代碼;如果 try中的代碼發生了異常(假設發生異常的代碼語句是xxx),那麼try中xxx下面的代碼就不會執行了,會立即跳轉到catch中去匹配異常,若匹配到了對應的catch中聲明的異常對象,那麼就執行該catch語句塊中的代碼,並且後面的catch語句塊就不再執行。等catch語句塊中的代碼執行完畢,就執行finally語句塊中的代碼。此時,整個try-catch-finally纔算執行完畢。
聲明拋出異常
聲明拋出異常是一個子句,它只能寫在方法頭部的後面,其格式如下:
throws <異常列表>
舉例:public void fun() throws IOException{......}
若在一個方法中聲明拋出異常,那麼調用該方法的調用者就必須對該異常進行處理,調用者處理該異常有兩種方式:
- 使用try-catch-finally來捕獲並處理該異常。
- 繼續拋出,留給後面的調用者去處理,從而形成異常處理鏈。
拋出異常
上面講到的聲明拋出異常只是告訴方法的調用者要去處理異常,這只是個說明性的語句,因爲方法的代碼中可能有異常,也可能沒有異常(沒有異常時就不會拋出)。
而真正拋出異常的語句是throw <異常對象>
,其中的異常異常對象必須是Throwable或其子類對象。例如:
throw new Exception("這是一個異常對象!")
當執行到上述語句時,會立即結束方法的執行。
接下來通過案例來講解,請看下面的代碼:
@Test
public void test1(){
try{
int a=1/0; //發生異常的代碼
System.out.println("算術異常!!!");
}
catch(ArithmeticException | NumberFormatException e){
System.out.println(e.getMessage()); //打印異常信息,並沒有拋出異常
throw new ArithmeticException("拋出算術異常!!!"); //拋出算術異常
}
finally{
System.out.println("執行finally代碼塊!!");
}
System.out.println("已經拋出算術異常!!!");
}
上述代碼的運行結果如下圖所示:
try中的int a=1/0;
語句發生了算術異常(除數爲0),直接跳轉到catch中去匹配異常對象,匹配到了ArithmeticException異常對象,然後執行catch中的代碼:打印異常信息,拋出異常。但是,在catch中拋出異常之前,會先執行finally中的代碼,等finally中的代碼執行完畢,再回到catch中拋出異常,而catch中的異常拋出後,整個方法就結束了,方法中剩下的代碼就不執行了。
以上就是上述代碼的完整執行流程。
異常屏蔽問題
如果在try-catch-finally代碼塊中,try、catch以及finally中都有異常拋出,那麼最終只能拋出finally代碼塊中的異常。這就是異常屏蔽問題。
請看下面的代碼:
//如果try、catch以及finally中都有異常拋出,那麼最終只能拋出finally中的異常,try和catch中的異常拋出會被屏蔽
@Test
public void test4(){
try{
System.out.println("try");
throw new RuntimeException("try中拋出異常!!!");
}
catch(Exception e){
System.out.println("catch");
throw new RuntimeException("catch中拋出異常!!!");
}
finally{
System.out.println("finally");
throw new RuntimeException("finally中拋出異常!!!");
}
}
上述代碼的運行結果爲:
首先執行try中的代碼:打印“try”
;然後在try中拋出異常,那麼在try中的異常拋出之前,要去catch中匹配異常對象並執行catch代碼塊,於是,匹配到了Exception對象並執行catch中的代碼:打印“catch”
,然後拋出異常;那麼,在拋出catch中的異常之前,要先執行finally中的代碼:打印“finally”
,並拋出finally中的異常
,此時整個方法執行結束,前面的兩個異常不會被拋出。這就是所謂的異常屏蔽。
其他問題
如果在tr-catch-finally代碼塊中有return,那麼執行流程又是怎樣的呢?
請看下面的代碼:
public int test5(){
try{
int a=1/0; //發生異常的代碼
return 1;
}
catch(Exception e){
System.out.println("catch");
return 2;
}
finally{
System.out.println("finally");
return 3; //禁止在finally中使用return語句,這裏只是舉例說明,在實際開發中禁止使用
}
}
在main方法中執行上述方法,結果爲:
try中int a=1/0;
語句發生了異常,那麼就跳轉到catch中去匹配並執行catch中的代碼,而catch中要返回整數2,在執行catch中的return 2;
語句之前,要先去執行finally中的代碼,因爲finally中有return 3;
語句,所以當執行到finally中的return語句後,整個方法就結束了,catch以及try中的return語句就不會執行了。
請繼續看下面的代碼:
public int test8(){
// try中沒有異常發生
try{
int a=1/1;
return 1;
}
catch(Exception e){
System.out.println("catch");
// throw new RuntimeException("catch拋出異常!!!");
return 2;
}
finally{
System.out.println("finally");
return 3; //禁止在finally中使用return語句,這裏只是舉例說明,在實際開發中禁止使用
}
}
上述代碼的返回的結果依舊是3。
public int test8(){
// try中沒有異常發生
try{
int a=1/1;
throw new Exception("try中的異常");
}
catch(Exception e){
System.out.println("catch");
return 2;
}
finally{
System.out.println("finally");
return 3; //禁止在finally中使用return語句,這裏只是舉例說明,在實際開發中禁止使用
}
}
上述代碼的運行結果如下圖所示:
try中沒有發生異常的代碼,但try中拋出了異常,那麼在拋出try中的異常之前,會先去匹配catch聲明的異常對象並執行catch中的代碼,因爲catch中有return語句,那麼在執行catch中的return語句之前,會先去執行finally中的代碼,因爲finally代碼塊中有return語句,所以當finally中的return語句執行完畢後,整個方法就結束了。前面的return語句就不會被執行了。
綜合上面的幾段代碼,我們來總結一下:
- 當try、catch以及finally中都拋出了異常時,只有finally中的異常會被拋出,try和catch中的異常會被屏蔽。
- 當try、catch以及finally中都有return語句時,只有finally中的return語句會被執行,try和catch中的return語句不會被執行。
- return和拋出異常(throw new XXXException())是不能同時出現在 同一個 代碼塊中的。
- 如果try中有return(throw)語句並且try中沒有異常,那麼在執行try中的return(throw)語句之前,要先去執行finally中的語句;同理,如果catch中有return(throw)語句,那麼在執行catch中的return(throw)語句之前,要先去執行finally中的代碼。
- 一旦執行了return語句(或throw拋出異常代碼),那麼該方法就立即結束了。
- 如果,在try、catch以及finally中混合了return語句和拋出異常代碼(throw),執行原理請參照第4條結論。
一句話總結一下:如果try、catch和finally中都有return語句(或者都拋出了異常),那麼只有finally中的return語句會執行(或者只有finally中的異常會被拋出),try和catch中的return語句不會被執行(或者try和catch中的異常不會被拋出)。
自定義異常類
自定義異常類必須是Throwable類的子類,通常是從Exception及其子類來繼承。例如,下述的MyException類就是一個自定義類。
public class MyException extends Exception {
}