1. 程式人生 > 實用技巧 >java-異常體系

java-異常體系

1 異常的繼承體系結構

  • Throwable 類是 Java 語言中所有錯誤或異常的超類。
  • 只有當物件是此類(或其子類之一)的例項時,才能通過 Java 虛擬機器或者 Java throw 語句丟擲。類似地,只有此類或其子類之一才可以是 catch 子句中的引數型別。
  • Throwable 包含了其執行緒建立時執行緒執行堆疊的快照。它還包含了給出有關錯誤更多資訊的訊息字串。
  • 最後,它還可以包含 cause(原因):另一個導致此 throwable 丟擲的 throwable。此 cause 設施在 1.4 版本中首次出現。它也稱為異常鏈 設施,因為 cause 自身也會有 cause,依此類推,就形成了異常鏈,每個異常都是由另一個異常引起的。

1.1 Error

  • Error 是 Throwable 的子類,用於指示合理的應用程式不應該試圖捕獲的嚴重問題
  • 大多數這樣的錯誤都是異常條件。雖然 ThreadDeath 錯誤是一個“正規”的條件,但它也是 Error 的子類,因為大多數應用程式都不應該試圖捕獲它。
  • 在執行該方法期間,無需在其 throws 子句中宣告可能丟擲但是未能捕獲的 Error 的任何子類,因為這些錯誤可能是再也不會發生的異常條件。
  • Java 程式通常不捕獲錯誤。錯誤一般發生在嚴重故障時,它們在Java程式處理的範疇之外

1.2 Exception

  • Exception 異常主要分為兩類

    • 一類是 IOException(I/O 輸入輸出異常),其中 IOException 及其子類異常又被稱作「受查異常」
    • 另一類是 RuntimeException(執行時異常),RuntimeException 被稱作「非受查異常」。
  • 受查異常就是指,編譯器在編譯期間要求必須得到處理的那些異常,你必須在編譯期處理了

1.2.1 常見的非檢查性異常:

下載.png

1.2.2 常見的檢查性異常:

2 自定義異常型別

Java 的異常機制中所定義的所有異常不可能預見所有可能出現的錯誤,某些特定的情境下,則需要我們自定義異常型別來向上報告某些錯誤資訊。

  • 在 Java 中你可以自定義異常。編寫自己的異常類時需要記住下面的幾點。
    • 所有異常都必須是 Throwable 的子類。
    • 如果希望寫一個檢查性異常類,則需要繼承 Exception 類。
    • 如果你想寫一個執行時異常類,那麼需要繼承 RuntimeException 類。

3 異常的處理方式

3.1 try...catch關鍵字

  • 使用 try 和 catch 關鍵字可以捕獲異常。
  • try/catch 程式碼塊放在異常可能發生的地方。

try/catch程式碼塊中的程式碼稱為保護程式碼,使用 try/catch 的語法如下:

try {
   // 程式程式碼
} catch(ExceptionName e1) {
   //Catch 塊
}
  • Catch 語句包含要捕獲異常型別的宣告。當保護程式碼塊中發生一個異常時,try 後面的 catch 塊就會被檢查。如果發生的異常包含在 catch 塊中,異常會被傳遞到該 catch 塊,這和傳遞一個引數到方法是一樣。
  • 一個 try 程式碼塊後面跟隨多個 catch 程式碼塊的情況就叫多重捕獲
  • 多重捕獲塊的語法如下所示:
try{
   // 程式程式碼
}catch(異常型別1 異常的變數名1){
  // 程式程式碼
}catch(異常型別2 異常的變數名2){
  // 程式程式碼
}catch(異常型別2 異常的變數名2){
  // 程式程式碼
}

3.2 throws/throw 關鍵字

  • 如果一個方法沒有捕獲一個檢查性異常,那麼該方法必須使用 throws 關鍵字來宣告。throws 關鍵字放在方法簽名的尾部。也可以使用 throw 關鍵字丟擲一個異常,無論它是新例項化的還是剛捕獲到的。
  • 下面方法的宣告丟擲一個 RemoteException 異常
public class className {
  public void deposit(double amount) throws RemoteException {
    // Method implementation
    throw new RemoteException();
  }
  //Remainder of class definition
}

一個方法可以宣告丟擲多個異常,多個異常之間用逗號隔開。

3.3 finally關鍵字

  • finally 關鍵字用來建立在 try 程式碼塊後面執行的程式碼塊。
  • 無論是否發生異常,finally 程式碼塊中的程式碼總會被執行。在 finally 程式碼塊中,可以執行清理型別等收尾善後性質的語句。
  • finally 程式碼塊出現在 catch 程式碼塊最後,語法如下:
try{
  // 程式程式碼
}catch(異常型別1 異常的變數名1){
  // 程式程式碼
}catch(異常型別2 異常的變數名2){
  // 程式程式碼
}finally{
  // 程式程式碼
}

4 try-catch-finally 的執行順序

try-catch-finally 執行順序的相關問題可以說是各種面試中的「常客」了,尤其是 finally 塊中帶有 return 語句的情況。我們直接看幾道面試題:

4.1 面試題一:

public static void main(String[] args){
    int result = test1();
    System.out.println(result);
}

public static int test1(){
    int i = 1;
    try{
        i++;
        System.out.println("try block, i = "+i);
    }catch(Exception e){
        i--;
        System.out.println("catch block i = "+i);
    }finally{
        i = 10;
        System.out.println("finally block i = "+i);
    }
    return i;
}

輸出結果如下

try block, i = 2
finally block i = 10
10

這算一個相當簡單的問題了,沒有坑,下面我們稍微改動一下

public static int test2(){
    int i = 1;
    try{
        i++;
        throw new Exception();
    }catch(Exception e){
        i--;
        System.out.println("catch block i = "+i);
    }finally{
        i = 10;
        System.out.println("finally block i = "+i);
    }
    return i;
}
catch block i = 1
finally block i = 10
10

4.2 面試題二

public static void main(String[] args){
    int result = test3();
    System.out.println(result);
}

public static int test3(){
    //try 語句塊中有 return 語句時的整體執行順序
    int i = 1;
    try{
        i++;
        System.out.println("try block, i = "+i);
        return i;
    }catch(Exception e){
        i ++;
        System.out.println("catch block i = "+i);
        return i;
    }finally{
        i = 10;
        System.out.println("finally block i = "+i);
    }
}

輸出結果如下:

try block, i = 2
finally block i = 10
2

是不是有點疑惑?明明我 try 語句塊中有 return 語句,可為什麼最終還是執行了 finally 塊中的程式碼?

我們反編譯這個類,看看這個 test3 方法編譯後的位元組碼的實現:

0: iconst_1         //將 1 載入進運算元棧
1: istore_0         //將運算元棧 0 位置的元素存進區域性變量表
2: iinc          0, 1   //將區域性變量表 0 位置的元素直接加一(i=2)
5: getstatic     #3     // 5-27 行執行的 println 方法                
8: new           #5                  
11: dup
12: invokespecial #6                                                     
15: ldc           #7 
17: invokevirtual #8                                                     
20: iload_0         
21: invokevirtual #9                                                     24: invokevirtual #10                
27: invokevirtual #11                 
30: iload_0         //將區域性變量表 0 位置的元素載入進操作棧(2)
31: istore_1        //把操作棧頂的元素存入區域性變量表位置 1 處
32: bipush        10 //載入一個常量到操作棧(10)
34: istore_0        //將 10 存入區域性變量表 0 處
35: getstatic     #3  //35-57 行執行 finally中的println方法             
38: new           #5                  
41: dup
42: invokespecial #6                  
45: ldc           #12                 
47: invokevirtual #8                  
50: iload_0
51: invokevirtual #9                
54: invokevirtual #10                 
57: invokevirtual #11                 
60: iload_1         //將區域性變量表 1 位置的元素載入進操作棧(2)
61: ireturn         //將操作棧頂元素返回(2)
-------------------try + finally 結束 ------------
------------------下面是 catch + finally,類似的 ------------
62: astore_1
63: iinc          0, 1
.......
.......
  • 從我們的分析中可以看出來,finally 程式碼塊中的內容始終會被執行,無論程式是否出現異常的原因就是,編譯器會將 finally 塊中的程式碼複製兩份並分別新增在 try 和 catch 的後面

可能有人會所疑惑,原本我們的 i 就被儲存在區域性變量表 0 位置,而最後 finally 中的程式碼也的確將 slot 0 位置填充了數值 10,可為什麼最後程式依然返回的數值 2 呢?

  • 仔細看位元組碼,你會發現在 return 語句返回之前,虛擬機器會將待返回的值壓入運算元棧,等待返回,即使 finally 語句塊對 i 進行了修改,但是待返回的值已經確實的存在於運算元棧中了,所以不會影響程式返回結果。

4.3 面試題三

public static int test4(){
    //finally 語句塊中有 return 語句
    int i = 1;
    try{
        i++;
        System.out.println("try block, i = "+i);
        return i;
    }catch(Exception e){
        i++;
        System.out.println("catch block i = "+i);
        return i;
    }finally{
        i++;
        System.out.println("finally block i = "+i);
        return i;
    }
}

執行結果:

try block, i = 2
finally block i = 3
3

其實你從它的位元組碼指令去看整個過程,而不要單單記它的執行過程。

你會發現程式最終會採用 finally 程式碼塊中的 return 語句進行返回,而直接忽略 try 語句塊中的 return 指令

最後,對於異常的使用有一個不成文的約定:儘量在某個集中的位置進行統一處理,不要到處的使用 try-catch,否則會使得程式碼結構混亂不堪。