java-異常體系
阿新 • • 發佈:2020-07-29
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 常見的非檢查性異常:
下載.png1.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,否則會使得程式碼結構混亂不堪。
。