1. 程式人生 > >java的異常處理機制詳解

java的異常處理機制詳解

異常機制已經成為判斷一門程式語言是否成熟的標準,異常機制可以使程式中異常處理程式碼和正常業務程式碼分離,保證程式程式碼更加優雅,並提高程式健壯性。


Java異常的處理主要依賴於try,catch,finally,throws,throw這五個關鍵字。下面分別介紹它們:
1. try:try塊中主要放置可能會產生異常的程式碼塊。如果執行try塊裡的業務邏輯程式碼時出現異常,系統會自動生成一個異常物件,該異常物件被提交給執行環境,這個過程被稱為丟擲(throw)異常。Java環境收到異常物件時,會尋找合適的catch塊(在本方法或是呼叫方法),如果找不到,java執行環境就會終止,java程式將退出
2. catch:catch塊中放置當出現相應的異常型別時,程式需要執行的程式碼。當try中語句可能發生多個異常的時候可以由多個catch。
3. finally:finally中存放一定會執行的程式碼,異常機制保證finally程式碼總是會被執行。當遇到try或catch中return或throw之類可以終止當前方法的程式碼時,jvm會先去執行finally中的語句,當finally中的語句執行完畢後才會返回來執行try/catch中的return,throw語句。如果finally中有return或throw,那麼將執行這些語句,不會在執行try/catch中的return或throw語句。finally塊中一般寫的是關閉資源之類的程式碼。
4. throws:在方法的簽名中,用於丟擲次方法中的異常給呼叫者,呼叫者可以選擇捕獲或者丟擲,如果所有方法(包括main)都選擇丟擲。那麼最終將會拋給JVM。JVM打印出棧軌跡(異常鏈)。
5. throw:用於丟擲一個具體的異常物件。


Java對異常的處理是按異常分類處理的,不同異常有不同的分類,每種異常都對應一個型別(class),每個異常都對應一個異常(類的)物件。java的系統定義的大致的異常類的層次圖如下(不全面,比如沒有SQLException等):


異常類層次圖




java中定義的異常(Exception)和錯誤(Error)都繼承自Throwable類。其中錯誤的產生大多是由於執行環境jvm導致的,這部分錯誤我們通過程式很難糾正,如果真的出現又必須糾正,那可能就要涉及到jvm調優的問題。如jvm的垃圾回收機制(GC)之類。
* 而Java的異常分為兩種,checked異常(編譯時異常)和Runtime異常(執行時異常)*
編譯時異常: java認為checked異常都是可以再編譯階段被處理的異常,所以它強制程式處理所有的checked異常,java程式必須顯式處理checked異常,如果程式沒有處理,則在編譯時會發生錯誤,無法通過編譯。
執行時異常:
在編譯的過程中,Runtime異常無須處理也可以通過編譯。所有的Runtime異常原則上都可以通過糾正程式碼來避免。

既然說java的異常都是一些異常類的物件,那麼這些異常類也有一些方法我們應該瞭解:
1. getMessage();返回該異常的詳細描述字元
2. printStackTrace():將該異常的跟蹤棧資訊輸出到標準錯誤輸出。(異常鏈)
3. printStackTrace(PrintStream s):將該異常的跟蹤棧資訊輸出到指定的輸出流
4. getStackTrace():返回該異常的跟蹤棧資訊。

再細講java是如何處理異常之前我們在來重申一下兩個重要問題


1. 為什麼要有異常?對於構造大型、健壯、可維護的應用系統而言,錯誤處理是整個應用需要考慮的重要方面。Java異常處理機制,在程式執行出現意外時,系統會生成一個Exception物件,來通知程式,從而實現將“業務功能實現程式碼”和“錯誤處理程式碼”分離,提供更好的可讀性。其中checked異常體現了java設計哲學:沒有完善處理的程式碼根本不會被執行,體現了java的嚴謹性。
2. 異常有什麼用? (1)可以對可能出現的異常進行更清晰的處理和說明,比如在finally中關閉資源或連線,或者在catch塊中捕獲異常列印資訊到螢幕和日誌等。(2)應用異常來處理業務邏輯,可以這麼做,但是這有違背異常設計的初衷(異常實質上可以是一個if else語句,當然可以用作業務處理)。

明確了上面兩個問題之後,我們就來看一下java的具體的異常處理機制。

1.使用try…catch捕獲異常:

java提出了一種假設,如果try中的語句一切正常那麼將不執行catch語句塊,如果try中語句出現異常,則會丟擲異常物件,由catch語句塊根據自己的型別進行捕獲。若沒有相應的catch塊,則丟擲。
所以其執行步驟可以總結為以下兩點:
(1) 如果執行try塊中的業務邏輯程式碼時出現異常,系統自動生成一個異常物件,該異常物件被提交給java執行環境,這個過程稱為丟擲(throw)異常。
(2) 當java執行環境收到異常物件時,會尋找能處理該異常物件的catch塊,如果找到合適的cathc塊並把該異常物件交給catch塊處理,那這個過程稱為捕獲(catch)異常;如果java執行時環境找不到捕獲異常的catch塊,則執行時環境終止,java程式也將退出。
下面還有幾點注意事項需要大家注意:
注意一: 不管程式程式碼塊是否處於try塊中,甚至包括catch塊中程式碼,只要執行該程式碼時出現了異常,系統都會自動生成一個異常物件,如果程式沒有為這段程式碼定義任何catch塊,java執行環境肯定找不到處理該異常的catch塊,程式肯定在此退出。
注意二: 進行異常捕獲時,一定要記住先捕獲小的異常,再捕獲大的異常。
注意三: 看下面一段java程式,我們來說明java對finally的處理方式:

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class Test{
    public static void main(String[] args) {
        FileInputStream fis=null;
        try {
            fis=new FileInputStream("a.txt");
        } catch (FileNotFoundException e) {
            System.out.println(e.getMessage());
            // return語句強制方法返回
            return;
            // 使用exit來退出虛擬機器
            //System.exit(1);
        }finally{
            if(fis!=null){
                try {
                    fis.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                fis=null;
            }
            System.out.println("fis資源已被回收");
        }
    }
}

執行這個程式,在catch中使用return而不exit可以得到如下結果:

a.txt (系統找不到指定的檔案。)
fis資源已被回收

如果使用exit而不是return,那麼將會得到如下結果:

a.txt (系統找不到指定的檔案。)

以上兩種情況顯示:除非在try塊或者catch塊中呼叫了退出虛擬機器的方法(即System.exit(1);),否則不管在try塊、catch塊中執行怎樣的程式碼,出現怎樣的情況,異常處理的finally塊總是會被執行的。當程式執行try塊,catch塊時遇到return語句或者throw語句,這兩個語句都會導致該方法立即結束,所以系統並不會立即執行這兩個語句,而是去尋找該異常處理流程中的finally塊,如果沒有finally塊,程式立即執行return語句或者throw語句,方法終止。如果有finally塊,系統立即開始執行finally塊,只有當finally塊執行完成後,系統才會再次跳回來執行try塊、catch塊裡的return或throw語句,如果finally塊裡也使用了return或throw等導致方法終止的語句,則finally塊已經終止了方法,不用再跳回去執行try塊、catch塊裡的任何程式碼了。所以,一般情況下,不要在finally塊中使用return或throw等導致方法終止的語句,因為一旦使用,將會導致try塊、catch塊中的return、throw語句失效。
以下四種情況將會導致finally塊不執行:
(1)在finally語句塊中發生了異常
(2)在前面的程式碼中使用了System.exit()退出虛擬機器
(3)程式所線上程死亡
(4)關閉cpu
- ps:在論壇上看到有朋友問:如果catch捕獲了異常,那麼try…catch語句塊之後的語句是否會執行?
- 答案是如果catch塊或finally塊中沒有throw語句或者return語句,那麼try…catch之後的語句就一定會執行。因為異常已經被捕獲和處理了呀~為什麼後面的語句為什麼不能執行呢。
- ps:可能又會有朋友問,如果try…catch塊之後的語句中有使用到try中的引用,而try中的語句失敗了,後面的怎麼執行?
- 放心,如果真的有這種情況,那java一定會要求你講這些語句和與那些可能失敗的語句一起放入try…catch塊中的。

2.使用throw(s)處理異常:

如果當前出現的異常在本方法中無法處理,我們只能丟擲異常。
如果每個方法都是簡單的丟擲異常,那麼在方法呼叫方法的多層巢狀呼叫中,Java虛擬機器會從出現異常的方法程式碼塊中往回找,直到找到處理該異常的程式碼塊為止。然後將異常交給相應的catch語句處理(異常沒有在本地處理,邏輯上throw之後的程式不會在進行)。如果Java虛擬機器追溯到方法呼叫棧最底部main()方法時,如果仍然沒有找到處理異常的程式碼塊(這屬於異常沒有得到處理,將終止出現異常的執行緒),將按照下面的步驟處理:
第一、呼叫異常的物件的printStackTrace()方法,列印方法呼叫棧的異常資訊。
第二、如果出現異常的執行緒為主執行緒,則整個程式執行終止;如果非主執行緒,則終止該執行緒,其他執行緒繼續執行。
關於throw的用法我們有幾點注意事項要注意:
注意一: throw語句後不允許有緊跟其他語句,因為這些沒有機會執行(塊外也不行,因為不會執行,無論是否被呼叫方捕獲。如果異常是在本方法內部throw直接捕獲,那是可以執行塊後面的程式碼的,記住只要throw論文,throw之後的程式碼都不會在執行)。我以一段程式來說明這個問題:

public class TestException {  
    public static void exc() throws ArithmeticException{
        int a =1;
        int b=4;
        for (int i=-2;i<3;i++){
                    a=4/i;
                System.out.println("i="+i);
        }
    }
    public static void caexc(){
        try {
            exc();
        } catch (ArithmeticException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {  
        TestException.caexc();      
    }  
}  

輸出結果為:

i=-2
i=-1
java.lang.ArithmeticException: / by zero
    at TestException.exc(TestException.java:8)
    at TestException.caexc(TestException.java:14)
    at TestException.main(TestException.java:21)

雖然捕獲了異常,但由於原來的執行緒已經throw,所以後面的程式碼均不會得到執行。
注意二: 如果一個方法呼叫了另外一個宣告丟擲異常的方法,那麼這個方法要麼處理異常,要麼宣告丟擲。
注意三: throw和throws關鍵字的區別:
throw用來丟擲一個異常,在方法體內。語法格式為:throw 異常物件。
throws用來宣告方法可能會丟擲什麼異常,在方法名後,語法格式為:throws 異常型別1,異常型別2…異常型別n
注意四: throw語句丟擲異常的兩種情況:
1.當throw語句丟擲的異常是Checked異常,則該throw語句要麼處於try塊裡顯式捕獲該異常,要麼放在一個帶throws宣告丟擲的方法中,即把異常交給方法的呼叫者處理。
2.當throw語句丟擲的異常是Runtime異常,則該語句無須放在try塊內,也無須放在帶throws宣告丟擲的方法中,程式既可以顯式使用try…catch來捕獲並處理該異常,也可以完全不理會該異常,把該異常交給方法的呼叫者處理。
Runtime異常和Checked異常在丟擲時的區別見下面這段程式碼:

public class TestException {  
    public static void throw_checked(int a) throws Exception{
        //Exception預設為checkedExcption
        if(a>0) throw new Exception("Exception:a>0");
    }
    public static void throw_runtime(int a) {
        if(a>0) throw new RuntimeException("runtimeException:a>0");
    }
    public static void main(String[] args) {  
        int a=1;
        try {
            throw_checked(a);
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        throw_runtime(a);
    }  
}  

可見Runtime異常的靈活性比Checked的靈活性更強。因為Checked異常必須要被顯式捕獲或者顯式丟擲,所以Runtime寫的更方便,我們自定義異常一般都是用Runtime異常。

3.自定義異常:

使用者自定義異常都應該繼承Exception基類,如果希望自定義Runtime異常,則應該繼承RuntimeException基類。異常類通常需要提供兩種構造器:一個是無引數的構造器,另一個是帶一個字串的構造器,這個字串將作為該異常物件的詳細說明(也就是異常物件的getMessage方法的返回值)。下面給出一段自定義異常MyException的程式碼:

public class MyException extends RuntimeException{
    public MyException(){
    }
    public MyException(String s){
        super(s);
    }
}

用throws宣告方法可能丟擲自定義的異常,並用throw語句在適當的地方丟擲自定義的異常。捕獲自定義異常的方法與捕獲系統異常一致。還可以異常轉型。

4.異常鏈(異常跟蹤棧):

異常物件的printStackTrace方法用於列印異常的跟蹤棧資訊,根據printStackTrace方法的輸出結果,我們可以找到異常的源頭,並跟蹤到異常一路觸發的過程。雖然printStackTrace()方法可以很方便地追蹤異常的發生狀況,可以用它來除錯,但是在最後釋出的程式中,應該避免使用它。而應該對捕獲的異常進行適當的處理,而不是簡單的將資訊打印出來。

5.總結:

(1) catch塊儘量保持一個塊捕獲一類異常,不要忽略捕獲的異常,捕獲到後要麼處理,要麼轉譯,要麼重新丟擲新型別的異常。
(2) 不要用try…catch參與控制程式流程,異常控制的根本目的是處理程式的非正常情況。
(3) 避免過大的try塊,不要把不會出現異常的程式碼放到try塊裡面,儘量保持一個try塊對應一個或多個異常。
(4) 細化異常的型別,不要不管什麼型別的異常都寫成Excetpion。

參考文獻: