天天看點

深入了解Java中的異常

雖然在編譯時發現錯誤是最理想的情況,但是這種情況并不是很容易産生,大多數的錯誤是在運作期間發生的,Java中的異常就是為了在運作時能夠檢查程式的錯誤。通過異常,我們能夠簡化錯誤代碼處理的邏輯,如果不使用異常,我們就要檢查特定的錯誤,這樣就會出現很多的

if...else...

語句,如果使用了異常機制則可以不必在方法實作中進行檢查,而是通過異常機制進行捕獲,這樣可以将方法實作的具體邏輯和遇到問題時的解決辦法解耦,而且如果遇到的異常能夠處理則進行捕獲處理,如果不能處理可以抛到上層調用API中,交由上層處理。

異常抛出會導緻原來程式執行的順序将會被終止,是以異常亦可以控制程式流程,當異常抛出後程式執行流程如下:

  • 程式使用new在堆上建立異常對象;
  • 目前執行路徑被終止,從目前環境擷取異常對象的引用;
  • 異常處理程式接管業務程式,以便将程式從錯誤狀态中恢複以使程式繼續運作或者通過另外的方式運作;

1. 異常的捕獲

所謂的異常捕獲,即對可能出現異常的代碼進行監控,如果處于監控區域的代碼發生異常則執行對應的異常處理程式,java中的文法支援即為

try...catch...

try{
  // 監控區域
}catch(Exception type1 e1){
  // 異常處理程式1
}catch(Exception type2 e2){
  // 異常處理程式2
}
           

具體的執行流程為:

  • 當try監控區域的代碼發生異常并抛出時,會首先在記憶體的堆上執行個體化該異常;
  • 将該異常對象順序的傳遞到每個catch語句進行比對,對應這種異常類型的catch語句中執行對應的異常處理程式,此處要注意隻會執行一個catch語句,之後的catch語句不會再執行;

有捕獲異常時在所有的catch語句塊中順序執行的機制,如果想捕獲所有的異常則可以在最後加上所有異常的基類

Exception

來比對所有的異常,但是對應該異常的catch語句一定要放到最後,以防它在其他異常之前被捕獲!

如上的try…catch…語句是最基本的,另外還發展出進化的異常捕獲語句:

// 用于資源自動關閉
try(){
    // 監控區域
}catch(Exception type1 e1){
    // 異常處理程式
}

// 用于在一個catch中捕獲兩種類型的異常
try{
  // 監控區域
}catch(Exception type1 | Exception type2 e){
  // 異常處理程式
}
           

對于捕獲的異常一般有兩種方法進行處理:

  • 終止程式的運作:一旦異常被抛出,由于無法采取有效的措施來彌補,是以隻能終止程式的運作;
  • 恢複程式的運作:通過一些措施來恢複程式的正常運作,比如将try語句放到while(true)循環中執行,通過在catch語句中修複程式達到程式正常運作,如下:
    while(true){
        try{
        // 可能發生異常的代碼
            break;
        }catch(Exception e){
        // 異常處理程式,恢複了程式的運作
        }
    }
               
    比如在發生

    FileNotFoundException

    時,可以通過在catch語句中所有檔案的上層和同級目錄來完成檢視檔案是否存在,以此來恢複程式的正常執行。

但是從實際開發角度來說,終止程式的運作可能會更常用,因為有時候我們并不能清楚的定位異常發生的原因,或者知道原因但是發現無法解決是以隻能抛出來終止程式的運作。

2. 異常體系分析

Java 中的異常繼承體系如下:

深入了解Java中的異常

上圖隻是Java提供的異常中的冰山一角,但是從繼承體系來說,所有的異常都是直接或者間接來自于

Throwable

這個類,

但是從圖上可以看出

Throwable

的直接子類有

Error

Exception

,那麼它們之間有什麼差別呢?

  • 首先是Error:這個類應該在系統内部使用,用來描述在編譯期間的系統級或者更底層的錯誤,一般來說我們不應該自定義這個類的子類,也不應試圖去捕獲它,常見的

    Error

    的子類,比如

    NoClassDefFoundError

  • 之後是最常見的也是我們使用最多的Exception,Exception又分為兩種
    • 一般的異常,也稱受檢異常:該類異常是在編譯期間發現的,一般來說這種異常是強制提醒開發者來進行處理以使程式恢複運作,而且也是可以通過一些措施來彌補的;
    • 運作時異常,也稱非受檢異常:這種異常是在程式運作期間抛出的,而且一般來說這種異常也無法由開發者處理,所有這種異常都會由虛拟機抛出,是以也就不必在異常說明語句

      throws

      後列舉,最常見的就是臭名昭著的

      NullPointerException

值得注意的問題是,除了

RuntimeException

異常,任何其他的異常都不應該忽略,因為對于

RuntimeException

類型的異常通常都是因為一些無法預料的問題,這些問題在開發者的預料之外,但是我們應該盡可能對一些代碼進行檢查和校驗以避免這種運作時異常,但是除了這種異常,其他的異常都應該被重視和處理。

3. 建立自定義異常

雖然

Java

提供給我們很多的異常類以供使用,但是出于業務的原因,我們可能更傾向于定義自己的符合業務需求的異常類,但是通常來說會有幾種原則遵循(自己的體會):

  1. 首先确定自定義的異常是能恢複的還是不能 的,這決定你定義的是受檢異常還是

    RuntimeException

    類型的異常,這要根據你想要采取的處理措施,如果你覺得發生這種情況根本無法處理,那就果斷選擇

    RuntimeException

    異常;
  2. 自定義的異常不要直接繼承自

    Throwable

    或者

    Exception

    ,而是繼承自意義相近的異常,比如當傳入的參數不對時,可以直接繼承

    IllegalArgumentException

  3. 由于異常類的本職工作就是用來處理錯誤情況的,是以其中不用添加過多的邏輯,因為可能基本上都用不上,隻需要添加相應的構造器和異常情況說明即可,而且構造器可以直接調用父類的構造器完成初始化;

下面通過一個具體的例子說明自定義異常的使用:

實際情況為:我們假設程式員發工資的時候需要輸入一個數值,這個數值當然不可能是負數(加班那麼長時間,看文檔比老婆次數還多,還給發負數!!),是以我們需要定義一個異常,當輸入的參數是負數時抛出這個異常。

根據上面說的原則進行對照:

  1. 輸入負數的時候我們有什麼辦法嗎?當然沒有,我們自己又不能把這個數變成正數(如果可以的話,我們還天天傻傻的對着電腦幹嘛!),是以是RuntimeException異常;
  2. Java提供的異常中有沒有對應的意義相近的異常,經過查找有就是我們上面說的

    IllegalArgumentException

    ,那麼我們的異常就直接繼承該異常即可;
  3. 這個異常會完成什麼功能,當然就是抛出這個錯誤,其餘什麼也不用幹,也幹不了!

OK,分析完可以開始定義異常如下:

public class ArgumentNegativeException extends IllegalArgumentException {
    public ArgumentNegativeException(){}
    public ArgumentNegativeException(String message){
        super(message);
    }
    public ArgumentNegativeException(String message, Throwable cause){
        super(message, cause);
    }
}
           

可以看出上面的異常類很簡單,隻是實作了三個父類的構造器,一般來說在自定義異常中實作這三個異常已經完全足夠,或者說也可以減少一些用不到的,下面定義我們的發工資的方法:

public static void testDispatch(int count) {
    if(count < ){
        throw new ArgumentNegativeException("f**k, the salary is negative");
    }
    // 全部上交
    // 老婆發給這個月的生活費
    // ......
}
           

從上邊可以看出由于我們自定義的異常是繼承自

RuntimeException

,是以不需要顯式使用

throws

抛出,而是在遇到異常時将該異常交給

JVM

來抛出!使用

testDispatch(-20000)

測試如下:

深入了解Java中的異常

順便說一句,RuntimeException異常是能捕獲的,但是平常從網絡看到的從沒有不捕獲過,這是因為這種異常設計的就是為了無法處理的情況,是以不建議捕獲,但是不建議捕獲不是不能捕獲,如下:

public void testException() {
    try{
        System.out.println("try statement");
       throw new ArgumentNegativeException("ArgumentNegativeException is thrown");
    }catch(ArgumentNegativeException e){
        System.out.println("catch statement: " + e.getLocalizedMessage());
    }
}
           

執行的結果如下:

深入了解Java中的異常

在使用異常的過程中,免不了要擷取抛出異常時的相關資訊,對于這一點,

Java

的設計者已經幫我們想到了,提供了許多有用的

API

,而這些API多數是由

Throwable

類提供的,如下:

  • getMessage()

    :擷取異常的詳細資訊;
  • getLocalizedMessage()

    :在Java内部實作實際上調用的是getMessage(),是以文檔建議我們覆寫官方的實作;
  • toString()

    :實際上該方法是對getLocalizedMessage()的包裝實作,如下:
    public String toString() {
        String s = getClass().getName();
        String message = getLocalizedMessage();
        return (message != null) ? (s + ": " + message) : s;
    }
               
  • printStackTrace(PrintStream s)

    等一系列的方法族:該系列

    API

    是将異常的堆棧資訊列印到列印流中,該資訊會顯示異常抛出的源頭在哪裡,可能也是我們最常見的異常方法,但是不建議直接使用該方法,而是在捕捉異常時将該異常資訊轉化為字元串作為一個消息傳回給調用者,如下:
    public static String stackTrace2String(Exception ex) {
        StringWriter writer = new StringWriter();
        ex.printStackTrace(new PrintWriter(writer));
        return writer.toString();
    }
               
  • getStackTrace()

    :該方法可以直接擷取printStackTrace()列印的資訊,該方法傳回一個由堆棧資訊組成的數組,每個元素表示方法調用序列中的一次調用,元素0為表示最後一個方法調用,也就是異常生成和抛出的地方,最後一個元素是第一個方法調用,

    getStackTraceDepth()

    能夠傳回調用深度;

基本上上面所說的幾個

API

已經能夠滿足我們的需要了。

4. 異常鍊

什麼是異常鍊?我們常常想在捕獲一個異常之後再抛出另外一個異常,并且想要儲存之前異常的資訊,進而将所有異常資訊聯系到一起,這個處理過程稱為異常鍊。這種處理場景一般發生在自定義異常時較多,比如想把系統抛出的異常轉譯為與業務相關的自定義異常。

那麼怎麼擷取異常之前的資訊呢?有兩種方法可以使用:

一種是通過構造器,通過

public Throwable(String message, Throwable cause){...}

或者

public Throwable(Throwable cause){...}

,然後通過getCause()方法将原始異常的原因擷取放到自定義異常中,以上面的自定義異常為例如下:

try{
    //...
}catch(NullPointerException e){
    // 将NullPointerException異常資訊放到新異常中
    throw new ArgumentNegativeException("", e.getCause());
}
           

另一種是通過

initCause()

,尤其是在異常的構造器中沒有Throwable參數時,使用該方法則是唯一選擇,如下:

try{
    //...
}catch(NullPointerException e){
    // 将NullPointerException異常資訊放到新異常中
    ArgumentNegativeException e1 = new ArgumentNegativeException("");
    e1.initCause(e);
    throw e1;
}
           

5. 蛋疼的Finally

Finally語句則是無論是否有異常抛出都會被執行,它的作用一般來說是進行資源釋放以及一些其他的後續的處理工作,但是涉及到具體的執行會有很多讓人迷惑的地方,比如和return語句誰先執行,關于 Java 中 finally 語句塊的深度辨析這篇文章說的很詳細也很深入,感覺自己不會比他分析的更好。