1. 程式人生 > >java深入淺出解析異常機制

java深入淺出解析異常機制

 

版權宣告:本文為博主原創文章,轉載請註明原地址,謝謝 https://blog.csdn.net/QuinnNorris/article/details/57428399

java中的異常處理的目的在於通過使用少量的程式碼,使得程式有著強大的魯棒性,並且這種異常處理機制會讓你變得非常自信:你的應用中沒有你沒處理過的錯誤。處理異常的相關手法看起來是這麼的難懂,但是如果掌握的話,會讓你的專案收益明顯,效果也會是立竿見影。 
如果我們不使用異常處理,那麼我們在程式中必須檢查特定的錯誤,並且在程式的很多地方去處理他,這樣會帶來低效率和高耦合。這是我們不希望看到的。有了異常處理,我們可以不必在方法呼叫處進行檢查,因為異常機制將保證能夠捕獲這個錯誤。並且,只需在一個地方處理錯誤。

(一)解決異常情形的基本思路

1.普通問題和異常情形

異常情形是指阻止當前方法或作用域繼續執行的問題。我們首先需要區分普通問題和異常情形,普通問題是在程式設計的過程中,我們可以通過已知的資訊解決,並繼續執行的問題。異常情形是指在當前的情況下,我們不能繼續下去了,在當前的環境下我們不能解決這個問題,我們只能將這個問題丟擲當前的環境,到一個大的環境中去企圖解決這個問題,這就是丟擲異常時發生的事情。

2.丟擲異常之後

我們在區分了普通問題和異常之後說到,如果我們沒有能力處理的問題就需要丟擲異常。在丟擲異常的時候,會有幾件事情發生:

  1. java用new在堆上來建立一個異常物件。
  2. 當前執行的程式不能被執行下去,程式被終止之後,從當前環境彈出一個對異常物件的引用。
  3. 異常處理機制接管程式,試圖找到一個恰當的地方來執行異常處理。

異常處理使得我們可以將每件事情都看作一個事物來處理,而異常可以看作這些事務的底線。我們還可以將異常看作是一種內建的恢復系統,因為我們的程式中可以擁有各種不同的恢復點。如果程式的某部分事物都失敗了,我們至少可以返回一個穩定的安全處。

3.兩種異常處理的基本模型

在所有現存的語言中,對於處理異常有兩種基本模型。 
java支援的模型是終止模型,(c++,c,python,c#也是如此)。在這種模型中,假設錯誤非常關鍵,以至於程式無法返回到異常發生的地方繼續執行程式。一旦異常被丟擲,說明程式已經無法挽回,不可能回到原處繼續進行了。 
另外一種稱為恢復模型

,這種模型認為異常處理程式的工作是修正錯誤,然後重新嘗試調用出問題的地方,並認為能夠二次成功。

雖然恢復模型開始顯得很吸引人,但不是很實用。其中的主要原因可能是它所導致的耦合,因為恢復性的處理程式需要了解異常丟擲的地點,這一點是致命的。我們怎麼才能告知程式我的程式碼在哪裡出錯呢?這勢必要包含了非通用性的高耦合程式碼,這增加了編寫和維護的難度。

(二)捕獲異常

就像我們上面說的,在丟擲異常的時候我們總是用new在堆上建立異常物件,這也伴隨著儲存空間的分配和構造器的呼叫。所有的標準異常類都有兩個構造器:一個是預設的構造器,另一個是接受字串作為引數。

1.將異常物件看作“返回”

關鍵詞throw會產生很多有趣的結果。在使用new建立了異常物件之後,此物件的引用將會傳遞給throw。儘管返回異常物件其型別通常與方法設計的返回型別不同,但從效果上看,我們可以假裝認為從這個方法或程式碼塊“返回”了一個異常物件給throw。

另外,我們可以丟擲任意型別的Throwable物件,他是異常類的根類(祖先類)。錯誤資訊可以儲存在異常物件內部或者用異常類的名稱來表示。

2.try塊

如果程式碼中丟擲異常,那麼我們的程式將會終止,如果不希望程式就此結束,我們可以通過try-catch塊來操作。在try塊中的內容如果丟擲了異常,我們只會結束try塊中執行的內容,而不會結束整個程式。

3.catch塊

catch塊就是我們剛才一直提到的異常處理程式。catch塊在try塊之後,我相信try-catch大家都知道,這裡也就不把這種基本的東西介紹個沒完了。需要注意的是,在try塊內部,可能有幾個方法都會丟擲同一個異常,我們只需寫一個這種異常的catch塊就可以捕獲所以,無需重複書寫。而且,catch塊要按照從細小到廣泛的順序來寫,如果我們把Exception放在第一個,那麼剩下的具體的異常catch塊將捕獲不到異常,因為Exception可以處理所有的。

關於catch塊的另外兩個小知識點: 
1.可以用|來合併那些雖然異常不同,但是操作相同的catch塊:

catch(FileNotFoundException | IOException e)
{
    //如果這兩個異常的操作是一樣的,我們可以把他們的操作寫在一起,從jdk7開始
}

2.如上捕獲多個異常的時候,異常變數隱含為final變數。不能為上面的程式碼的e賦不同的值。

4.建立一個自己定義的異常類

我們先建立一個自己定義的異常類,我們可以看到,異常類有這樣的兩種構造器方法,一種是預設的無引數構造方法,另外一種是傳遞一個String型別的出錯原因的構造器。(異常一共有4種構造器方法,這裡只說了兩種)我們什麼都不用做,因為我們現在只是簡單的看一下,具體的內容我們以後再來新增。

package ExceptionEx;

/**
 * 
 * @author QuinnNorris
 * 
 *         自定義異常類
 */
public class MyException extends Exception {

    public MyException() {

    }

    public MyException(String msg) {
        super(msg);
    }

}

之後,我們要建立一個測試類,在這個測試類中我們會讓一個方法手動的丟擲一個異常。這種方法只是為了演示,在實際的情況下,異常物件地丟擲都是因為我們的編譯問題由編譯器為我們丟擲的。在我們用方法丟擲一個異常後,我們會用try-catch塊來自己接住這個異常。

package ExceptionEx;

/**
 * 
 * @author QuinnNorris
 * 
 *         測試類,用方法丟擲異常
 */
public class TestExcep {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        try {
            throwMyExce();
        } catch (MyException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    public static void throwMyExce() throws MyException {
        throw new MyException();
    }

}

輸出的結果是我們出錯的棧軌跡,之所以會輸出這些內容,是我們在catch塊中呼叫printStackTrace方法的結果。

輸出結果: 
ExceptionEx.MyException 
at ExceptionEx.TestExcep.throwMyExce(TestExcep.java:20) 
at ExceptionEx.TestExcep.main(TestExcep.java:12)

(三)異常類繼承關係樹

1.異常類關係圖

java異常類關係樹 
看一張圖,一看就懂。

2.Exception和Error類

在java中,所有的異常都有一個共同的祖先 Throwable類。而Throwable類有兩個子類,一個是Error類,一個是Exception,這兩個哥倆分別管兩種不同型別的錯誤。

Error類主要負責“錯誤”。 
這個錯誤和異常有很大的區別。你可以看見我們上面一直使用的名詞是異常而不是錯誤。因為在java中,能通過程式碼處理的我們叫做異常,而我們不能處理的才叫做錯誤。錯誤指的是那些例如:JVM執行錯誤,棧空間用盡,類定義錯誤等等非常嚴重的問題。這種問題我們是無法去處理的,也就是說,程式一旦出現Error我們和編譯器都是沒辦法解決的。java只能盡全力去試圖讓程式碼遠離error,但是如果發生,我們都無能為力了。

Exception類主要負責“異常”。 
這個類中的問題我們是可以去通過程式碼處理的。所以有的時候我們說異常處理的這種機制,往往會想到Exception。這是沒問題的,而且其實Throwable類的方法和Exception是相同的,連構造器都是那麼類似,Exception主要負責處理類似:空指標、除數為零、陣列越界這類問題。

2.執行時異常、非執行時異常

在Exception中,我們通常又將異常分為兩類,RunTimeException和其他的異常。其他的異常也有很多種,但是我們為什麼把執行時異常(RunTimeException)單獨提出來作為一類呢?因為這裡有本質性的區別。

RunTimeException表示那些邏輯性錯誤,可以避免。 
執行時異常都包含那些呢?我們看兩個例子就懂了:NullPointerException(空指標異常)、IndexOutOfBoundsException(下標越界異常)。在我們編寫程式碼的時候,我們從來不會希望編寫出有空指標或者下標越界的程式碼,因為這是錯誤的,在java中,這些邏輯上的,因為我們大意而造成的錯誤都叫做執行時錯誤。執行時錯誤是不會在程式中用try-catch塊來宣告的,因為如果出現這種錯誤我們會修改自己的程式碼糾正錯誤,而不是用try-catch塊來捕獲。

注:如果你非要用try-catch塊來包含自己的程式碼免受RunTimeException的困擾,從理論上編譯器是不會報錯的。但是這樣只會讓你的程式碼變得冗長,而且你會因此而不知道你的邏輯到底有沒有問題。這是一種非常不好的行為。

非執行時異常表示有可能發生的異常,我們需要宣告。 
有的時候我們會遇到這種情況:在使用檔案操作的時候,突然有一句語法正確的話被編譯器報錯,當滑鼠移動到上面的時候發現,哦,原來是讓我throw或者try-catch。這種就是非執行時異常。為了防止程式碼在執行時出現問題,java強制規定:非執行時異常必須被處理。這種強制規定的原因前面已經講過了,總之,當要使用檔案或者SQL語句的時候,出現例如檔案找不到,資料庫失敗這樣的問題是非常有可能的,我們必須丟擲或處理這種可能會出現的異常。

一句話來總結:執行異常是程式邏輯錯誤,無法預料,改正就可以,無需丟擲或處理。非執行時異常是顯然可能存在的錯誤,我們被強制必須丟擲或處理來保證程式的安全性。

3.已檢查的異常、未檢查的異常

通常,Java的異常(Throwable)又可以被分為已檢查異常(checked exceptions)和未檢查異常(unchecked exceptions)這兩種。其實這兩種和我們上面的差不多,我們在這裡給出一下概念:

已檢查異常: 
包括非執行時異常。在程式中是可以被檢查的,需要我們處理和預防的。

未檢查的異常: 
包括RuntimeException和Error。我們在程式和邏輯上儘量避免出現這種異常,如果出現這種異常我們是未知的。

(四)Execption類

上面我們分析了Throwable類的兩個子類,既然我們不去管Error,我們來看一下Exception類的結構。

1.getMessage、getLocalizedMessage

我們剛才說過,Exception類有這樣兩種構造器(異常類一共四種構造器),有引數的構造器有一個String的引數,是用來儲存錯誤資訊的,那麼既然能夠儲存資訊,也肯定有讀取資訊的方法。

public String getMessage()
//返回此 throwable 的詳細訊息字串。 

public String getLocalizedMessage()
//建立此 throwable 的本地化描述。子類可以重寫此方法,以便生成特定於語言環境的訊息。
//對於不重寫此方法的子類,預設實現返回與 getMessage() 相同的結果。 

我們通過這兩種方法來獲得儲存著資訊的字串。

2.printStackTrace

printStackTrace這個方法有三種過載的形態,總的來說,這個方法的作用是輸出錯誤資訊。

public void printStackTrace()
//將此 throwable 及其追蹤輸出至標準錯誤流。
//此方法將此 Throwable 物件的堆疊跟蹤輸出至錯誤輸出流,作為欄位 System.err 的值。
//輸出的第一行包含此物件的 toString() 方法的結果。
//剩餘行表示以前由方法 fillInStackTrace() 記錄的資料。

public void printStackTrace(PrintStream s)
//將此 throwable 及其追蹤輸出到指定的輸出流。 

public void printStackTrace(PrintWriter s)
//將此 throwable 及其追蹤輸出到指定的 PrintWriter。 

我們給出一個API中的例子:

 class MyClass {
     public static void main(String[] args) {
         crunch(null);
     }
     static void crunch(int[] a) {
         mash(a);
     }
     static void mash(int[] b) {
         System.out.println(b[0]);
     }
 }

輸出b[0]的時候,這個陣列是不存在的,肯定是一個空指標的錯誤。通過上面的這個這個例子會產生以下的錯誤。

 java.lang.NullPointerException
         at MyClass.mash(MyClass.java:9)
         at MyClass.crunch(MyClass.java:6)
         at MyClass.main(MyClass.java:3)

3.fillInStackTrace

在上一個printStackTrace顯示棧軌跡的方法中有說過,棧軌跡是儲存在這個方法中的。

public Throwable fillInStackTrace()
//在異常堆疊跟蹤中填充。此方法在 Throwable 物件資訊中記錄有關當前執行緒堆疊幀的當前狀態。 
  • 1
  • 2

4.getStackTrace、setStackTrace

我們還有以陣列的形式獲得和修改棧軌跡的方法,但是在一般情況下,我們幾乎不會用到,這裡就展示一下。

public StackTraceElement[] getStackTrace()
//提供程式設計訪問由 printStackTrace() 輸出的堆疊跟蹤資訊。
//返回堆疊跟蹤元素的陣列,每個元素表示一個堆疊幀。
//陣列的第零個元素(假定資料的長度為非零)表示堆疊頂部,它是序列中最後的方法呼叫。

public void setStackTrace(StackTraceElement[] stackTrace)
//設定將由 getStackTrace() 返回,並由 printStackTrace() 和相關方法輸出的堆疊跟蹤元素。 
//此方法設計用於 RPC 框架和其他高階系統,允許客戶端重寫預設堆疊跟蹤,

(五)如何捕獲全部異常?

我們剛才說了如何去捕獲異常,也分析了Exception類中的方法,那麼我們現在要提出一個有實際性意義的問題:如何去捕獲全部的異常?你可能會不理解這句話的意思,但實際上,我們在日常編寫程式碼執行的時候有可能因為幾次方法的呼叫、不同異常的丟擲導致最開始丟擲的異常丟失。如果我們要將異常用日誌的形式記錄下來,這種異常情況的丟失是我們不能容忍的。

1.棧軌跡

棧軌跡是一種概念。我們的異常存放於一個異常堆疊中,這個棧有棧頂,和一幀幀的位置來儲存異常被丟擲的路徑。

 java.lang.NullPointerException
         at MyClass.mash(MyClass.java:9)
         at MyClass.crunch(MyClass.java:6)
         at MyClass.main(MyClass.java:3)

這是printStackTrace方法打印出來的棧軌跡,這個方法返回一個由棧軌跡中的元素所構成的陣列。每個元素表示棧中的一個幀。陣列中的最後一個元素也是棧底也是呼叫序列中離丟擲異常最遠的第一個方法呼叫。

2.重拋異常

在很多個方法遞迴呼叫的情況下,有的時候我們用try-catch塊接住了異常,但是我們並不想在這個方法中處理,而是想把它繼續向上丟擲,到上一個塊中處理。那麼這個時候我們就需要重新丟擲異常

重拋異常會把異常拋給上一級環境中的異常處理程式,同一個try塊的後續catch子句將會被忽略。此外,異常物件的所有資訊都得以保持。所以高一級的環境中捕獲此異常的處理程式可以從這個異常物件中得到所有資訊。

需要注意的是,如果我們多次重拋異常,在這之後我們再呼叫printStackTrace()方法,現實的棧軌跡是全部的軌跡,而不是最後一次丟擲時開始的軌跡,這是我們希望的情況。同時,還有另外一件事,fillInStackTrace()方法會返回一個異常物件,會重新整理當前的棧軌跡,也就是說我們呼叫fillInStackTrace的話,那一行就變成了異常的新發生地了。那麼這兩件事情結合在一起,我們得出了一個結論:多次重拋同一個異常,不會呼叫fillInStackTrace方法重新整理棧軌跡。但是還有另外的情況,就是我們在捕獲異常之後丟擲了另一個異常。因為這樣做會替換那個異常物件的堆疊,就相當於使用了fillInStackTrace()方法,重拋了不同的異常,會導致有關原來的異常發生點的資訊會丟失,剩下的是新的異常的丟擲點的資訊

package Test;  

/**
 * 
 * @author QuinnNorris
 * 
 * 重拋異常測試類
 */
public class Test{  
    public Test() {  
    }  

    void testEx() throws Exception {  
        try {  
            testEx1();  
        } catch (Exception e) {  
            throw e;  
        }
    }  

    void testEx1() throws Exception {   
        try {  
           testEx2();  
        } catch (Exception e) {  
            //throw e;
            //throw new Exception();
        }
    }  

    void testEx2() throws Exception {  
        try {  
            int b = 1;  
            int c;  
            for (int i = 2;; i--) {  
                c = b / i;  
            }  
        } catch (Exception e) {  
            throw e;  
        } 
    }  

    public static void main(String[] args) throws Exception {  
        Test testException1 = new Test();   
            testException1.testEx();  
    }  
}  

這個程式碼模擬了我們平時工作的方法多層呼叫的情況。

throw e;
  •  

我們先把這個程式碼中註釋掉的第一行去掉註釋。

輸出結果: 
Exception in thread “main” java.lang.ArithmeticException: / by zero 
at Test.Test.testEx2(Test.java:35) 
at Test.Test.testEx1(Test.java:23) 
at Test.Test.testEx(Test.java:15) 
at Test.Test.main(Test.java:44)

這個時候,它的棧軌跡是這樣的,這個是完全的從除零錯誤開始的站軌跡。

throw new Exception();
  • 1

這個時候我們再一次註釋掉第一行,我們把第二行的註釋去掉。

輸出結果:Exception in thread “main” java.lang.Exception 
at Test.Test.testEx1(Test.java:26) 
at Test.Test.testEx(Test.java:15) 
at Test.Test.main(Test.java:44)

完全變成了另外一個異常。這說明了這種重拋異常的時候可能發生的問題,可能上面的例子確實很簡單,但是實際中,當我們許多個方法一呼叫可能就會被這種情況搞糊塗了。

3.異常鏈

常常會想要在捕獲一個異常後跑出另一個異常,並且希望能將原來的異常資訊儲存下來,這種我們稱之為異常鏈。在Throwable的子類構造器中都有一個可以接受一個叫做cause的Throwable物件作為引數,表述原始的那個異常。但是這個構造器只有幾個異常類(Throwable,Error,Exception,RuntimeException)擁有,更普遍的寫法是呼叫initCause方法。

public Throwable(Throwable cause)
//構造一個帶指定 cause 和 (cause==null ? null :cause.toString())的詳細訊息的新 throwable。
//此構造方法對於那些與其他 throwable(例如,PrivilegedActionException)的包裝器相同的 throwable 來說是有用的。 
//呼叫 fillInStackTrace() 方法來初始化新建立的 throwable 中的堆疊跟蹤資料。 


public Throwable(String message,Throwable cause)
//構造一個帶指定詳細訊息和 cause 的新 throwable。
//注意,與 cause 相關的詳細訊息不是 自動合併到這個 throwable 的詳細訊息中的。 
//呼叫 fillInStackTrace() 方法來初始化新建立的 throwable 中的堆疊跟蹤資料。 

public Throwable initCause(Throwable cause)
//將此 throwable 的 cause 初始化為指定值。(該 Cause 是導致丟擲此 throwable 的throwable。) 
//此方法至多可以呼叫一次。此方法通常從構造方法中呼叫,或者在建立 throwable 後立即呼叫。
//如果此 throwable 通過 Throwable(Throwable) 或 Throwable(String,Throwable) 建立,此方法不能呼叫。 

我們需要注意的是最後一行寫出的,initCause和構造器方法不相容的情況,其他的沒問題。 
下面我們要舉出一個使用異常鏈的具體例子,這個例子是來自thinking in java的,可能比較長,但他確實比其他書中的例子更加直觀和有實際意義。

package ExceptionEx;

/**
 * 
 * @author QuinnNorris
 * 
 *         繼承了Exception的自定義異常類
 */
class DynamicFieldException extends Exception {
}

/**
 * 
 * @author QuinnNorris
 * 
 *         包含一個物件陣列以及一些操作的主類
 */
public class DynamicFields {

    private Object[][] fields;

    /**
     * 用傳入的大小引數設定初始化陣列的行數
     * 
     * @param initialSize
     */
    public DynamicFields(int initialSize) {
        fields = new Object[initialSize][2];
        for (int i = 0; i < initialSize; i++)
            fields[i] = new Object[] { null, null };
    }

    /**
     * 重寫toString,以為了能夠輸出陣列
     */
    public String toString() {
        StringBuilder result = new StringBuilder();
        for (Object[] field : fields) {
            result.append(field[0]);
            result.append(": ");
            result.append(field[1]);
            result.append("\n");
        }
        return result.toString();
    }

    /**
     * 查詢是否有是id的field[n][0]
     * 
     * @param id
     * @return
     */
    private int hasField(String id) {
        for (int i = 0; i < fields.length; i++)
            if (id.equals(fields[i][0]))
                return i;
        return -1;
    }

    /**
     * 查詢id的索引,如果有就返回索引,如果沒有就丟擲異常
     * 
     * @param id
     * @return
     * @throws NoSuchFieldException
     */
    private int getFieldNumber(String id) throws NoSuchFieldException {
        int idIndex = hasField(id);
        if (idIndex == -1)
            throw new NoSuchFieldException();
        return idIndex;
    }

    /**
     * 檢視全部的fields,將一個新的id新增進去。如果沒有空位置就將陣列新增一行返回索引。
     * 
     * @param id
     * @return
     */
    private int makeField(String id) {
        for (int i = 0; i < fields.length; i++)
            if (fields[i][0] == null) {
                fields[i][0] = id;
                return i;
            }
        Object[][] tmp = new Object[fields.length + 1][2];
        for (int i = 0; i < fields.length; i++)
            tmp[i] = fields[i];
        for (int i = fields.length; i < tmp.length; i++)
            tmp[i] = new Object[] { null, null };
        fields = tmp;
        return makeField(id);
    }

    /**
     * 獲取id的那一行儲存的值
     * 
     * @param id
     * @return
     * @throws NoSuchFieldException
     */
    public Object getField(String id) throws NoSuchFieldException {
        return fields[getFieldNumber(id)][1];
    }

    /**
     * 從id中取出值
     * 
     * @param id
     * @param value
     * @return
     * @throws DynamicFieldException
     */
    public Object setField(String id, Object value)
            throws DynamicFieldException {
        if (value == null) {
            DynamicFieldException dfe = new DynamicFieldException();
            dfe.initCause(new NullPointerException());
            throw dfe;
        }
        int fieldNumber = hasField(id);
        if (fieldNumber == -1)
            fieldNumber = makeField(id);
        Object result = null;
        try {
            result = getField(id);
        } catch (NoSuchFieldException e) {
            throw new RuntimeException(e);
        }
        fields[fieldNumber][1] = value;
        return result;
    }

    /**
     * @param args
     */
    public static void main(String[] args) {
        DynamicFields df = new DynamicFields(3);
        System.out.println(df);

        try {
            df.setField("d", "A value for d");
            df.setField("number", 47);
            df.setField("number2", 48);
            System.out.println(df);
            df.setField("d", "A new value for d");
            df.setField("number3", 11);
            System.out.println("df: " + df);
            Object field = df.setField("d", null);
        } catch (DynamicFieldException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    }

}

每個物件都含有一個數組,元素是”成對的物件“。第一個物件表示欄位識別符號,第二個標識欄位值,值的型別可以是除基本型別外的任意型別。當呼叫setField方法的時候,她將試圖通過標識修改已有欄位值,否則就建一個新的欄位,並把值放入。如果空間不夠了,將建立一個更長的陣列,並把原來的陣列的元素複製進去。如果你試圖為欄位設定一個空值,將丟擲一個DynamicFieldException異常,它是通過使用initCause方法把NullPointerException物件插入而建立的。

在這裡我們還有另外一個很重要的技巧:

public Object setField(String id, Object value)
            throws DynamicFieldException {
        if (value == null) {
            DynamicFieldException dfe = new DynamicFieldException();
            dfe.initCause(new NullPointerException());
            throw dfe;
        }
        int fieldNumber = hasField(id);
        if (fieldNumber == -1)
            fieldNumber = makeField(id);
        Object result = null;
        try {
            result = getField(id);
        } catch (NoSuchFieldException e) {
            throw new RuntimeException(e);
        }

或許你會為上面這段程式碼感到疑惑,為什麼我們不直接在方法名後面宣告NoSuchFieldException異常,而是捕獲這個異常之後,將他變成RuntimeException。原因是這樣的:如果在一個方法中發生了一個已檢查異常,而不允許丟擲它,那麼我們可以捕獲這個已檢查異常,並將它包裝成一個執行時異常。這個方法在這裡看來有些多餘,但是這種將已檢查異常包裝在未檢查異常裡的手法非常實用,在後面我們會介紹到,如果我們繼承的方法沒有宣告丟擲異常,我們是不能丟擲異常的,到那個時候,我們會需要到這種手法。

(六)使用finally進行清理

1.finally作用

對於一些程式碼,可能會希望無論try塊中的異常是否丟擲,它們都能得到執行。為了達到這個效果,可以在異常處理程式後面加上finally子句。對於沒有垃圾回收和解構函式自動呼叫機制的語言來說,finally非常重要。它能使程式設計師保證:無論try塊中發生什麼,記憶體總能得到釋放。

但是在java中有垃圾回收機制,所以記憶體釋放不再是問題。所以,更多的時候,當要把除了記憶體以外的資源恢復到它們的初始狀態時,就要用到finally子句。這種資源包括:已經開啟的檔案或網路資源,在頻幕上畫的圖形等等。稍微有一些專案經驗的朋友就知道finally到底多麼好用。

2.帶資源的try塊

在很多的情況下,我們用finally只是為了簡單的將資源關閉。再注意到這種情況後,java se7為這種情況提供了一個很有用的快捷方式。可以為我們快速簡潔的關閉用.close方法關閉的資源。

try(Resource res = ...)
{
    //do some work
}

在這種寫法之下,try快退出時,會自動呼叫res.close()方法。但是這種寫法僅僅在是“close”方法的時候可用,比如多執行緒中的ReentrantLock的關閉方式不是呼叫close方法,那這就不適用。

3.finally的異常丟失問題

遺憾的是,java中的異常實現有一些瑕疵。異常作為程式出錯的標誌,絕對不應該被忽略,如果被忽略會給除錯者帶來很大的麻煩。但是,請考慮這種情況:在try中呼叫了方法,這個方法丟擲一個一場,但是在finally中又呼叫了其他的一個方法,這個新方法也丟擲一個異常。理論上,當代碼遇到異常的時候,會直接停止現在的工作,丟擲異常,但是finally這種特殊的機制,導致又丟擲了一個異常,而且這種丟擲直接導致前面的異常被覆蓋了。

甚至還有更令人絕望的問題,比如,你可以試著敲一下下面這段程式碼。

package ExceptionEx;

public class FinallyEx {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        try {
            throw new RuntimeException();
        } finally{
            return;
        }

    }

}

你會發現,這個程式不會產生任何輸出。 
這是一種相當嚴重的缺陷,因為異常可能會以一種比前面的例子更加微妙和難以察覺的方式完全丟失。在平時的專案中,我們要做的就是儘量少的把邏輯性程式碼放入finally中,finally最主要的作用還應該是關閉資源。

4.finally塊中的程式碼一定會執行嗎?

所有的書上都在說,finally塊一定會被執行,但是如果你去面試,面試官很有可能會問你:“finally塊一定會被執行麼?”這個時候,你就蒙了。事實上finally塊在一種情況下不會被執行:JVM被退出。虛擬機器都被推出了,什麼都不會執行了。

package ExceptionEx;

public class FinallyEx {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        try {
            System.exit(0); 

        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }finally{
            //do some thing;
            System.out.println("111");
        }

    }

}

如果呼叫System.exit(0)那麼你會發現不會再打印出111這個字串了。還有請不要在丟擲異常後呼叫這個退出jvm的方法,編譯器會報一個:Unreachable code(程式碼不會被執行)的錯誤。

(七)異常丟擲的限制

如果在子類中覆蓋了超類的一個方法,子類方法中宣告的已檢查異常不能比超類方法中宣告的異常更通用。特別需要說明的是,如果超類方法沒有丟擲任何已檢查異常,子類也不能丟擲任何已檢查異常。

1.如何處理這種限制

這個時候我們或許會要用到上文中提到過的包裝方法。將已檢查異常當作未檢查異常偷樑換柱丟擲去。所以有的時候我們說:不要去丟擲任何未檢查異常,這麼絕對的說也是不好的。

(八)異常機制使用技巧

目前還是存在著大量的對於如何恰當的使用異常機制的爭論。有很多人認為異常還不夠,但是也有很多人認為這種繁瑣的機制應該直接被去掉。下面有一些關於異常機制使用的技巧:

1.異常處理不能代替簡單測試

我們有很多的情況下需要判斷一個值是不是為空,根據它的情況來進行下一步的操作。在這種時候,我們還是用if-else的判斷更佳,如果這種簡單測試都用try-catch來捕獲,那麼這程式碼也確實過於冗雜。

2.不要過分的細化也不要只捕獲Throwable

很多程式設計師都習慣將每條語句都放在一個try塊中,這樣的結果只會導致程式碼量的急劇膨脹。不要過分的細化每個異常,但是我們也不要只捕獲Throwable,只丟擲RuntimeException異常。這樣也會讓你的程式碼更加的難以理解。

3.不要壓制異常

在java中既然有try-catch,我們常常會直接在方法內部關閉掉這個異常,直接把異常壓制在非常低的等級中。這不是非常好的做法。儘管有的異常我們不需要處理,但是還有一些異常,丟擲來的話我們才能知道到底是哪裡出了問題。

4.不要害怕丟擲異常,“苛刻”的檢查比“放任”好

丟擲異常不代表程式出了bug,丟擲很多異常不代表我們程式碼問題很多,如果放任異常往往導致要花費更多的時間去慢慢的找問題出現的地點。報錯或丟擲異常總是比結果錯誤好。不僅如此,自己丟擲一個EmptyStackException總是比java自己丟擲一個NullPointerException好。