1. 程式人生 > >Java 異常模型綜述

Java 異常模型綜述

wal little 誤報 tag 虛擬 tca lines 報告 開發

一. 異常的引入及基礎

  發現錯誤的理想時機是在編譯階段。也就是在你試圖運行程序之前。

然而,編譯期間編譯器並不能找出全部的錯誤,余下的錯誤僅僅有在運行期才幹發現和解決,這類錯誤就是 Throwable。

這就須要錯誤源能夠通過某種方式,把適當的信息傳遞給某個接收者。該接收者將知道怎樣正確的處理這個問題,這就是Java的錯誤報告機制 —— 異常機制。該機制使得程序把 在正常運行過程中做什麽事的代碼 出了問題怎麽辦的代碼 相分離。
  
  在對異常的處理方面。Java 採用的是 終止模型

在這樣的模型中,將假設錯誤非常關鍵。以至於程序無法返回到異常發生的地方繼續運行。一旦異常被拋出,就表明錯誤已經無法挽回。也不能回來繼續運行。

相對於終止模型,還有一種異常處理模型為 恢復模型。它使異常被處理之後能夠繼續運行程序。盡管該模型非常吸引人,但不是非常實用,其主要原因是它所導致的耦合:恢復性處理程序須要了解異常的拋出地點。這勢必要包括依賴於拋出位置的非通用代碼,從而大大添加了代碼編寫和維護的難度。
  
  在異常情形中,異常的拋出伴隨著以下三件事的發生:

  • 首先,同 Java 中其它對象的創建一樣,將使用 new 在堆上創建異常對象。
  • 其次,當前的運行路徑被終止 。而且從當前環境中彈出對異常對象的引用。
  • 最後,異常處理機制接管程序 ,並開始尋找相應的異常處理程序,並將程序從錯誤狀態中恢復。

二. Java 標準異常

1.基本概念

             技術分享
                         異常類層次結構演示樣例
                         
- Throwable:全部的異常類型的根類

  在 Java 中。Throwable 是全部的異常類型的根類。Throwable 有兩個直接子類:ExceptionError。二者都是 Java 異常處理的重要子類,各自都包括大量子類。
 
- Error:程序本身無法處理的錯誤

  Error 是程序無法處理的錯誤,表示運行應用程序中較嚴重問題。這些錯誤大部分與代碼編寫者運行的操作無關。而與代碼運行時的 JVM 、資源等有關。比如,Java虛擬機運行錯誤(Virtual MachineError),當 JVM 不再有繼續運行操作所需的內存資源時,將出現 OutOfMemoryError。這些異常發生時。Java虛擬機(JVM)通常會選擇線程終止。這些錯誤是不可查的,而且它們在應用程序的控制和處理能力之外。

在 Java 中。錯誤通過Error的子類描寫敘述。

  • Exception:程序本身能夠處理的錯誤

      Exception 一般是Java程序猿所關心的。其在Java類庫、用戶方法及運行時故障中都可能拋出。它由兩個分支組成: 運行時異常(派生於 RuntimeException 的異常)其它異常劃分這兩種異常的規則是:由程序錯誤(一般是邏輯錯誤。如錯誤的類型轉換、數組越界等。應該避免)導致的異常屬於RuntimeException;而程序本身沒有問題。但由於諸如I/O這類錯誤(eg:試圖打開一個不存在的文件)導致的異常就屬於其它異常。


此外,Java的異常(包括Exception和Error)通常可分為 受檢查的異常(checked exceptions)不受檢查的異常(unchecked exceptions) 兩種類型。

  • 不受檢查異常:派生於 Error 或 RuntimeException 的全部異常

      不可查異常是編譯器不要求強制處理的異常,包括運行時異常(RuntimeException與其子類)和錯誤(Error)。

    也就是說,當程序中可能出現這類異常,即使沒實用try-catch語句捕獲它,也沒實用throws子句聲明拋出它。也會編譯通過。

  • 受檢查異常:除去不受檢查異常的全部異常

      受檢查異常是編譯器要求必須處理的異常。

    這裏所指的處理方式有兩種: 捕獲並處理異常 聲明拋出異常 。也就是說。當程序中可能出現這類異常,要麽用 try-catch 語句捕獲它,要麽用 throws 子句聲明拋出它,否則編譯不會通過。

  • 準則:假設程序出現RuntimeException異常,那麽一定是程序猿的問題

  • 異常和錯誤的差別:異常能被程序本身處理。錯誤則無法處理


三. Java 異常處理機制

  1. 異常處理

      在 Java 應用程序中。異常處理機制為:拋出異常捕捉異常
      
      拋出異常: 當一個方法出現錯誤引發異常時。方法創建異常對象並交付運行時系統,異常對象中包括了異常類型和異常出現時的程序狀態等異常信息。運行時系統負責尋找處理異常的代碼並運行。

      捕獲異常: 在方法拋出異常之後,運行時系統將轉為尋找合適的異常處理器(exception handler)。潛在的異常處理器是異常發生時依次存留在調用棧中的方法的集合。當異常處理器所能處理的異常類型與方法拋出的異常類型相符時。即為合適 的異常處理器。運行時系統從發生異常的方法開始,依次回查調用棧中的方法。直至找到含有合適異常處理器的方法並運行。

    當運行時系統遍歷調用棧而未找到合適的異常處理器,則運行時系統終止。同一時候。意味著 Java 程序的終止。


  對於運行時異常、錯誤或受檢查的異常。Java技術所要求的異常處理方式有所不同:

  • 由於運行時異常是不受檢查的。Java規定:運行時異常將由Java運行時系統自己主動拋出,同意應用程序忽略運行時異常;

  • 對於方法運行中可能出現的Error,當運行方法不欲捕捉時,Java同意該方法不做不論什麽拋出聲明。由於。大多數Error是不可恢復的,也屬於合理的應用程序不該捕捉的異常。

  • 對於全部受檢查的異常,Java規定:異常必須被捕捉,或者進行異常說明。也就是說,當一個方法選擇不捕捉可查異常時。它必須聲明將拋出異常。

      不論什麽Java代碼都能夠拋出異常,如:自己編寫的代碼、來自Java開發環境包中代碼,或者Java運行時系統。不管是誰。都能夠通過Java的 throw 語句拋出異常。

      整體來說,Java規定:對於可查異常必須捕捉、或者聲明拋出。同意忽略不可查的 RuntimeException 和 Error。


2、 異常說明

  對於受檢查異常而言,Java提供了相應的語法。使你能告知client程序猿某個方法可能會拋出的異常類型,然後client程序猿就能夠進行相應的處理。這就是異常說明。它屬於方法聲明的一部分。緊跟在形式參數列表之後,如以下的代碼所看到的:

void f() throws TooBig, TooSmall, DivZero { ... }

表示 方法f 可能會拋出TooBig, TooSmall, DivZero三種異常,而

void g() { ... ... }

表示 方法g 不會拋出不論什麽異常。

  代碼必須與異常說明保持一致。

若方法中的代碼產生了受檢查異常卻沒有進行處理,編譯器就會發現這個問題並提醒你:要麽處理這個異常,要麽在異常說明中表明此方法將產生異常。只是,我們能夠聲明方法將拋出異常。但實際上並不拋出。


3、捕獲異常

監控區域:它是一段可能產生異常的代碼,而且後面跟著處理這些異常的代碼,由 try…catch… 子句 實現。

  (1) try 子句
  假設方法內部拋出了異常,這種方法將在拋出異常的過程中結束。若不希望方法就此結束。能夠在方法內設置一個特殊的塊來捕獲異常。當中,在這個塊裏,嘗試各種方法調用的部分稱為 try 塊:

try { 
    // Code that might generate exceptions 
} 

   (2) catch 子句 – 異常處理程序
  拋出的異常必須得到處理。而且針對每一個要捕獲的異常。都必須準備相應的異常處理程序。異常處理程序必須緊跟在 try塊 之後,以 catch 關鍵字表示:

try { 
  // Code that might generate exceptions 
} catch(Type1 id1)|{ 
  // Handle exceptions of Type1 
} catch(Type2 id2) { 
  // Handle exceptions of Type2 
} catch(Type3 id3) { 
  // Handle exceptions of Type3 
} 

  異常處理程序可能用不到標識符(id1,id2,…)。由於異常的類型本身就已經給出了足夠的信息來處理異常,但標識符不可省。

當異常被拋出時,異常處理機制將負責搜尋參數與異常類型相匹配的第一個處理程序。

然後進入相應的catch自居運行。此時覺得異常得到處理。一旦catch子句結束,則處理程序的查找結束(與 switch…case…不同)。
  
  特別須要註意的是:
  

  • 異常匹配原則:拋出異常時,異常處理系統會依照代碼的書寫順序找出近期匹配(派生類的對象能夠匹配其基類的處理程序)的處理程序。

    一旦找到,它就覺得異常將得到處理,然後停止查找;

  • 不可屏蔽派生類異常:捕獲基類異常的catch子句必須放在捕獲其派生類異常的catch子句之後,否則編譯不會通過。

  • catch子句 必須與 try子句 連用


   (3) finally 子句

  • The finally Block Description

      The finally block always executes when the try block exits. This ensures that the finally block is executed even if an unexpected exception occurs. But finally is useful for more than just exception handling — it allows the programmer to avoid having cleanup code accidentally bypassed by a return,continue, or break. Putting cleanup code in a finally block is always a good practice, even when no exceptions are anticipated.
      
      Note: If the JVM exits while the try or catch code is being executed, then the finally block may not execute. Likewise, if the thread executing the try or catch code is interrupted or killed, the finally block may not execute even though the application as a whole continues.


  • finally 子句 總會被運行(前提:相應的 try子句 運行)
     
     以下代碼就沒有運行 finally 子句:
 public class Test { 
    public static void main(String[] args) { 
        System.out.println("return value of test(): " + test()); 
    } 

    public static int test() { 
        int i = 1; 
        System.out.println("the previous statement of try block"); 
        i = i / 0; 

        try { 
            System.out.println("try block"); 
            return i; 
        }finally { 
            System.out.println("finally block"); 
        } 
     } 
 }/* Output:
        the previous statement of try block 
        Exception in thread "main" java.lang.ArithmeticException: / by zero 
        at com.bj.charlie.Test.test(Test.java:15) 
        at com.bj.charlie.Test.main(Test.java:6)
 *///:~ 

  當代碼拋出一個異常時,就會終止方法中剩余代碼的運行,同一時候退出該方法的運行。

假設該方法獲得了一些本地資源。而且這些資源(eg:已經打開的文件或者網絡連接等)在退出方法之前必須被回收,那麽就會產生資源回收問題。這時,就會用到finally子句。示比例如以下:

InputStream in = new FileInputStream(...);

try{
    ...
}catch (IOException e){
    ...
}finally{
    in.close();
}

  • finally 子句與控制轉移語句的運行順序

      A finally clause can also be used to clean up for break, continue and return, which is one reason you will sometimes see a try clause with no catch clauses. When any control transfer statement is executed, all relevant finally clauses are executed. There is no way to leave a try block without executing its finally clause.

      先看四段代碼:

// 代碼片段1
 public class Test { 
    public static void main(String[] args) {  
        try {  
            System.out.println("try block");  
            return ;  
        } finally {  
            System.out.println("finally block");  
        }  
    }  
 }/* Output:
        try block 
        finally block
 *///:~ 
// 代碼片段2
public class Test { 
    public static void main(String[] args) {  
        System.out.println("reture value of test() : " + test()); 
    } 

    public static int test(){ 
        int i = 1; 

        try {  
            System.out.println("try block");  
            i = 1 / 0; 
            return 1;  
        }catch (Exception e){ 
            System.out.println("exception block"); 
            return 2; 
        }finally {  
            System.out.println("finally block");  
        } 
    } 
}/* Output:
        try block 
        exception block 
        finally block 
        reture value of test() : 2
 *///:~ 
// 代碼片段3
public class ExceptionSilencer { 
    public static void main(String[] args) { 
        try { 
            throw new RuntimeException(); 
        } finally { 
            // Using ‘return’ inside the finally block 
            // will silence any thrown exception. 
            return; 
        } 
    } 
} ///:~
// 代碼片段4
class VeryImportantException extends Exception { 
    public String toString() {return "A very important exception!"; } 
} 

class HoHumException extends Exception { 
    public String toString() { 
        return "A trivial exception"; 
    } 
} 

public class LostMessage { 
    void f() throws VeryImportantException { 
        throw new VeryImportantException(); 
    } 
    void dispose() throws HoHumException { 
        throw new HoHumException(); 
    } 
    public static void main(String[] args) { 
        try { 
            LostMessage lm = new LostMessage(); 
            try { 
                lm.f(); 
            } finally { 
                lm.dispose(); 
            } 
        } catch(Exception e) { 
        System.out.println(e); 
        } 
    } 
} /* Output: 
   A trivial exception 
*///:~

  從上面的四個代碼片段,我們能夠看出,finally子句 是在 try 或者 catch 中的 return 語句之前運行的。更加一般的說法是,finally子句 應該是在控制轉移語句之前運行。控制轉移語句除了 return 外,還有 break 和 continue。另外,throw 語句也屬於控制轉移語句。盡管 return、throw、break 和 continue 都是控制轉移語句,可是它們之間是有差別的。當中 return 和 throw 把程序控制權轉交給它們的調用者(invoker),而 break 和 continue 的控制權是在當前方法內轉移。


  以下。再看兩個代碼片段:

// 代碼片段5
public class Test { 
    public static void main(String[] args) { 
        System.out.println("return value of getValue(): " + getValue()); 
    } 

    public static int getValue() { 
        try { 
                return 0; 
        } finally { 
                return 1; 
            } 
     } 
 }/* Output:
        return value of getValue(): 1
 *///:~ 
// 代碼片段6
public class Test { 
    public static void main(String[] args) { 
        System.out.println("return value of getValue(): " + getValue()); 
    } 

    public static int getValue() { 
        int i = 1; 
        try { 
                return i; 
        } finally { 
                i++; 
        } 
    } 
 }/* Output:
         return value of getValue(): 1
 *///:~ 

  利用我們上面分析得出的結論:finally子句 是在 try子句 或者 catch子句 中的 return 語句之前運行的。 由此。能夠輕松的理解代碼片段 5 的運行結果是 1。由於 finally 中的 return 1。語句要在 try 中的 return 0;語句之前運行,那麽 finally 中的 return 1;語句運行後。把程序的控制權轉交給了它的調用者 main()函數,而且返回值為 1。
  
  那為什麽代碼片段 6 的返回值不是 2,而是 1 呢? 依照代碼片段 5 的分析邏輯,finally 中的 i++;語句應該在 try 中的 return i;之前運行啊? i 的初始值為 1,那麽運行 i++;之後為 2,再運行 return i。那不就應該是 2 嗎?怎麽變成 1 了呢?
  
  關於 Java 虛擬機是怎樣編譯 finally 子句的問題,有興趣的讀者能夠參考《 The JavaTM Virtual Machine Specification, Second Edition 》中 7.13 節 Compiling finally。

那裏具體介紹了 Java 虛擬機是怎樣編譯 finally 子句。

實際上。Java 虛擬機會把 finally 子句作為 subroutine 直接插入到 try 子句或者 catch 子句的控制轉移語句之前。可是,還有另外一個不可忽視的因素,那就是在運行 subroutine(也就是 finally 子句)之前。try 或者 catch 子句會保留其返回值到本地變量表(Local Variable Table)中。待 subroutine 運行完成之後,再恢復保留的返回值到操作數棧中,然後通過 return 或者 throw 語句將其返回給該方法的調用者(invoker)。

  請註意,前文中我們以前提到過 return、throw 和 break、continue 的差別,對於這條規則(保留返回值)。僅僅適用於 return 和 throw 語句,不適用於 break 和 continue 語句,由於它們根本就沒有返回值。

  以下再看最後三個代碼片段:

// 代碼片段7
public class Test { 
    public static void main(String[] args) { 
        System.out.println("return value of getValue(): " + getValue()); 
    } 

@SuppressWarnings("finally") 
public static int getValue() { 
    int i = 1; 
    try { 
            i = 4; 
        } finally { 
            i++; 
            return i; 
        } 
    } 
 }/* Output:
        return value of getValue(): 5
 *///:~
// 代碼片段8
public class Test { 
    public static void main(String[] args) { 
        System.out.println("return value of getValue(): " + getValue()); 
    } 

 public static int getValue() { 
    int i = 1; 
    try { 
            i = 4; 
        } finally { 
            i++; 
        } 
        return i; 
     } 
 }/* Output:
        return value of getValue(): 5
 *///:~
// 代碼片段9
public class Test { 
    public static void main(String[] args) {  
        System.out.println(test());  
    }  

    public static String test() {  
        try {  
            System.out.println("try block");  
            return test1();  
        } finally {  
            System.out.println("finally block");  
        }  
    }  
    public static String test1() {  
        System.out.println("return statement");  
        return "after return";  
    }  
}/* Output:
        try block 
        return statement 
        finally block 
        after return
 *///:~

  請註意,最後個案例的唯一一個須要註意的地方就是,return test1(); 這條語句等同於 :

 String tmp = test1(); 
 return tmp;

  因而會產生上述輸出。


  特別須要註意的是,在以下4種特殊情況下,finally子句不會被(全然)運行:
  
  1)在 finally 語句塊中發生了異常;
  2)在前面的代碼中用了 System.exit()【JVM虛擬機停止】退出程序;
  3)程序所在的線程死亡;
  4)關閉 CPU;


四. 異常的限制

 
  當覆蓋方法時。僅僅能拋出在基類方法的異常說明裏列出的那些異常。這意味著,當基類使用的代碼應用到其派生類對象時,一樣能夠工作。
  

class BaseballException extends Exception {} 
class Foul extends BaseballException {} 
class Strike extends BaseballException {} 

abstract class Inning { 
    public Inning() throws BaseballException {} 
    public void event() throws BaseballException { 
        // Doesn’t actually have to throw anything 
    } 
    public abstract void atBat() throws Strike, Foul; 
    public void walk() {} // Throws no checked exceptions 
} 

class StormException extends Exception {} 
class RainedOut extends StormException {} 
class PopFoul extends Foul {} 

interface Storm { 
    public void event() throws RainedOut; 
    public void rainHard() throws RainedOut; 
} 

public class StormyInning extends Inning implements Storm { 
    // OK to add new exceptions for constructors, but you must deal with the base constructor exceptions: 
    public StormyInning() throws RainedOut, BaseballException {} 

    public StormyInning(String s) throws Foul, BaseballException {} 

    // Regular methods must conform to base class: 
    void walk() throws PopFoul {}   //Compile error 

    // Interface CANNOT add exceptions to existing methods from the base class: 
    public void event() throws RainedOut {} 

    // If the method doesn’t already exist in the base class, the exception is OK: 
    public void rainHard() throws RainedOut {} 

    // You can choose to not throw any exceptions, even if the base version does: 
    public void event() {} 

    // Overridden methods can throw inherited exceptions: 
    public void atBat() throws PopFoul {} 

    public static void main(String[] args) { 
        try { 
            StormyInning si = new StormyInning(); 
            si.atBat(); 
        } catch(PopFoul e) { 
            System.out.println("Pop foul"); 
        } catch(RainedOut e) { 
            System.out.println("Rained out"); 
        } catch(BaseballException e) { 
            System.out.println("Generic baseball exception"); 
        } 

        // Strike not thrown in derived version. 
        try { 

            // What happens if you upcast?

----印證“編譯器的類型檢查是靜態的,是針對引用的!!

。” Inning i = new StormyInning(); i.atBat(); // You must catch the exceptions from the base-class version of the method: } catch(Strike e) { System.out.println("Strike"); } catch(Foul e) { System.out.println("Foul"); } catch(RainedOut e) { System.out.println("Rained out"); } catch(BaseballException e) { System.out.println("Generic baseball exception"); } } } ///:~

  • 異常限制對構造器不起作用

      子類構造器不必理會基類構造器所拋出的異常。然而。由於基類構造器必須以這樣或那樣的方式被調用(這裏默認構造器將自己主動被調用),派生類構造器的異常說明必須包括基類構造器的異常說明。

  • 派生類構造器不能捕獲基類構造器拋出的異常

      由於 super() 必須位於子類構造器的第一行,而若要捕獲父類構造器的異常的話,則第一行必須是 try 子句。這樣會導致編譯不會通過。

  • 派生類所重寫的方法拋出的異常列表不能大於父類該方法的異常列表,即前者必須是後者的子集

      通過強制派生類遵守基類方法的異常說明,對象的可替換性得到了保證。須要指出的是,派生類方法能夠不拋出不論什麽異常,即使基類中相應方法具有異常說明。也就是說,一個出如今基類方法的異常說明中的異常,不一定會出如今派生類方法的異常說明裏。

  • 異常說明不是方法簽名的一部分

      盡管在繼承過程中,編譯器會對異常說明做強制要求。但異常說明本身並不屬於方法類型的一部分,方法類型是由方法的名字及其參數列表組成。因此,不能基於異常說明來重載方法。


五. 自己定義異常

  使用Java內置的異常類能夠描寫敘述在編程時出現的大部分異常情況。

除此之外,用戶還能夠自己定義異常。用戶自己定義異常類。僅僅需繼承Exception類就可以。


  
  在程序中使用自己定義異常類,大體可分為以下幾個步驟:

  (1)創建自己定義異常類;
  (2)在方法中通過throw關鍵字拋出異常對象;
  (3)假設在當前拋出異常的方法中處理異常。能夠使用try-catch語句捕獲並處理。否則在方法的聲明處通過throws關鍵字指明要拋出給方法調用者的異常,繼續進行下一步操作;
  (4)在出現異常方法的調用者中捕獲並處理異常。


六. 異常棧與異常鏈

1、棧軌跡

  printStackTrace() 方法能夠打印Throwable和Throwable的調用棧軌跡。調用棧顯示了由異常拋出點向外擴散的所經過的全部方法。即方法調用序列(main方法 一般是方法調用序列中的最後一個)。


2、又一次拋出異常

catch(Exception e) { 
System.out.println("An exception was thrown"); 
throw e; 
} 

  既然已經得到了對當前異常對象的引用,那麽我們就能夠像上面一樣將其又一次拋出。又一次拋出的異常會把異常拋給上一級環境中的異常處理程序。同一個try子句的興許catch子句將被忽略。

此外。假設僅僅是把當前異常對象又一次拋出,那麽printStackTrace() 方法顯示的仍是原來異常拋出點的調用棧信息,而並不是又一次拋出點的信息。要想更新這個信息,能夠調用fillInStackTrace() 方法,這將返回一個Throwable對象。它是通過把當前調用棧信息填入原來那個異常對象而建立的。


  
看以下演示樣例:

public class Rethrowing { 
    public static void f() throws Exception { 
        System.out.println("originating the exception in f()"); 
        throw new Exception("thrown from f()"); 
    } 

    public static void g() throws Exception { 
        try { 
            f(); 
        } catch(Exception e) { 
            System.out.println("Inside g(),e.printStackTrace()"); 
            e.printStackTrace(System.out); 
            throw e; 
        } 
    } 

    public static void h() throws Exception {
        try { 
            f(); 
        } catch(Exception e) { 
            System.out.println("Inside h(),e.printStackTrace()"); 
            e.printStackTrace(System.out); 
            throw (Exception)e.fillInStackTrace(); 
        } 
    } 

    public static void main(String[] args) { 
        try { 
            g(); 
        } catch(Exception e) { 
            System.out.println("main: printStackTrace()"); 
            e.printStackTrace(System.out); 
        } 
        try { 
            h(); 
        } catch(Exception e) { 
            System.out.println("main: printStackTrace()"); 
            e.printStackTrace(System.out); 
        } 
    } 
} /* Output: 
    originating the exception in f() 
    Inside g(),e.printStackTrace() 
    java.lang.Exception: thrown from f() 
    at Rethrowing.f(Rethrowing.java:7) 
    at Rethrowing.g(Rethrowing.java:11) 
    at Rethrowing.main(Rethrowing.java:29) 
    main: printStackTrace() 
    java.lang.Exception: thrown from f() 
    at Rethrowing.f(Rethrowing.java:7) 
    at Rethrowing.g(Rethrowing.java:11) 
    at Rethrowing.main(Rethrowing.java:29) 
    originating the exception in f() 
    Inside h(),e.printStackTrace() 
    java.lang.Exception: thrown from f() 
    at Rethrowing.f(Rethrowing.java:7) 
    at Rethrowing.h(Rethrowing.java:20) 
    at Rethrowing.main(Rethrowing.java:35) 
    main: printStackTrace() 
    java.lang.Exception: thrown from f() 
    at Rethrowing.h(Rethrowing.java:24) 
    at Rethrowing.main(Rethrowing.java:35) 
*///:~ 

3、異常鏈

 異常鏈:在捕獲一個異常後拋出還有一個異常。而且希望把原始異常的信息保存下來。
 
  這能夠使用帶有cause參數的構造器(在Throwable的子類中,僅僅有Error,Exception和RuntimeException三個類提供了帶有cause的構造器)或者使用initcause()方法把原始異常傳遞給新的異常。使得即使在當前位置創建並拋出了新的異常,也能通過這個異常鏈追蹤到異常最初發生的位置。比如:

class DynamicFieldsException extends Exception {}

...

DynamicFieldsException dfe = new DynamicFieldsException(); 
dfe.initCause(new NullPointerException()); 
throw dfe;

...

//捕獲該異常並打印其調用站軌跡為:
/**
DynamicFieldsException 
at DynamicFields.setField(DynamicFields.java:64) 
at DynamicFields.main(DynamicFields.java:94) 
Caused by: java.lang.NullPointerException 
at DynamicFields.setField(DynamicFields.java:66) 
... 1 more
*/

以 RuntimeException 及其子類NullPointerException為例,其源代碼分別為:
  
  RuntimeException 源代碼包括四個構造器。有兩個可接受cause:

public class RuntimeException extends Exception {
    static final long serialVersionUID = -7034897190745766939L;

    /** Constructs a new runtime exception with <code>null</code> as its
     * detail message.  The cause is not initialized, and may subsequently be
     * initialized by a call to [email protected] #initCause}.
     */
    public RuntimeException() {
    super();
    }

    /** Constructs a new runtime exception with the specified detail message.
     * The cause is not initialized, and may subsequently be initialized by a
     * call to [email protected] #initCause}.
     *
     * @param   message   the detail message. The detail message is saved for 
     *          later retrieval by the [email protected] #getMessage()} method.
     */
    public RuntimeException(String message) {
    super(message);
    }

    /**
     * Constructs a new runtime exception with the specified detail message and
     * cause.  <p>Note that the detail message associated with
     * <code>cause</code> is <i>not</i> automatically incorporated in
     * this runtime exception‘s detail message.
     *
     * @param  message the detail message (which is saved for later retrieval
     *         by the [email protected] #getMessage()} method).
     * @param  cause the cause (which is saved for later retrieval by the
     *         [email protected] #getCause()} method).  (A <tt>null</tt> value is
     *         permitted, and indicates that the cause is nonexistent or
     *         unknown.)
     * @since  1.4
     */
    public RuntimeException(String message, Throwable cause) {
        super(message, cause);
    }

    /** Constructs a new runtime exception with the specified cause and a
     * detail message of <tt>(cause==null ? null : cause.toString())</tt>
     * (which typically contains the class and detail message of
     * <tt>cause</tt>).  This constructor is useful for runtime exceptions
     * that are little more than wrappers for other throwables.
     *
     * @param  cause the cause (which is saved for later retrieval by the
     *         [email protected] #getCause()} method).  (A <tt>null</tt> value is
     *         permitted, and indicates that the cause is nonexistent or
     *         unknown.)
     * @since  1.4
     */
    public RuntimeException(Throwable cause) {
        super(cause);
    }
}

  NullPointerException 源代碼僅包括兩個構造器。均不可接受cause:

public class NullPointerException extends RuntimeException {
    /**
     * Constructs a <code>NullPointerException</code> with no detail message.
     */
    public NullPointerException() {
    super();
    }

    /**
     * Constructs a <code>NullPointerException</code> with the specified 
     * detail message. 
     *
     * @param   s   the detail message.
     */
    public NullPointerException(String s) {
    super(s);
    }
}

註意:

  全部的標準異常類都有兩個構造器:一個是默認構造器。還有一個是接受字符串作為異常說明信息的構造器。


引用:

《Java編程思想(第四版)》
深入理解java異常處理機制
關於 Java 中 finally 語句塊的深度辨析

Java 異常模型綜述