1. 程式人生 > 實用技巧 >java中的異常機制

java中的異常機制

一、異常概述

異常是發生在程式執行過程中阻礙程式正常執行的錯誤事件,當一個程式出現錯誤時,可能的情況有如下3種:
1、語法錯誤:程式碼語法出現了錯誤,此類錯誤可通過IDE的智慧提示糾正。
2、執行時錯誤:空指標異常,陣列越界,除數為零等,此類錯誤IDE無法檢測出來,只有當程式執行之後才能察覺。
3、邏輯錯誤:執行結果與預想的結果不一樣,此類錯誤往往需要通過除錯程式碼才能找出。

Java中的異常處理機制主要處理執行時錯誤。

二、異常分類

java中異常分類如下圖所示:

從上圖中可以看到,所有的異常都繼承自一個共同的父類Throwable,而Throwable有兩個重要的子類:Exception(異常)和Error(錯誤)。
對Exception(異常)和Error(錯誤)介紹如下:

1、Exception(異常):是程式本身可以處理的異常
主要包含RuntimeException等執行時異常和IOException,SQLException等非執行時異常。
(1)執行時異常包括:都是RuntimeException類及其子類異常,如NullPointerException(空指標異常)、IndexOutOfBoundsException(下標越界異常)等,這些異常是不檢查異常,程式中可以選擇捕獲處理,也可以不處理。這些異常一般是由程式邏輯錯誤引起的,程式應該從邏輯角度儘可能避免這類異常的發生。 執行時異常的特點是Java編譯器不會檢查它,即當程式中可能出現這類異常,即使沒有用try-catch語句捕獲它,也沒有用throws子句宣告丟擲它,也會編譯通過。

(2)非執行時異常(編譯異常)包括:RuntimeException以外的異常,型別上都屬於Exception類及其子類。從程式語法角度講是必須進行處理的異常,如果不處理,程式就不能編譯通過。如IOException、SQLException等以及使用者自定義的Exception異常,一般情況下不自定義檢查異常。

2、Error(錯誤):是程式無法處理的錯誤,表示執行應用程式中較嚴重問題
大多數錯誤與程式碼編寫者執行的操作無關,而表示程式碼執行時 JVM(Java 虛擬機器)出現的問題。例如,Java虛擬機器執行錯誤(Virtual MachineError),當 JVM 不再有繼續執行操作所需的記憶體資源時,將出現 OutOfMemoryError。這些異常發生時,Java虛擬機器(JVM)一般會選擇執行緒終止。 這些錯誤表示故障發生於虛擬機器自身、或者發生在虛擬機器試圖執行應用時,如Java虛擬機器執行錯誤(Virtual MachineError)、類定義錯誤(NoClassDefFoundError)等。這些錯誤是不可查的,因為它們在應用程式的控制和處理能力之外,而且絕大多數是程式執行時不允許出現的狀況。對於設計合理的應用程式來說,即使確實發生了錯誤,本質上也不應該試圖去處理它所引起的異常狀況。在 Java中,錯誤通過Error的子類描述。


從編譯器是否要求強制處理的角度分類,異常類別又可分為:可查異常和不可查異常
(1)可查異常:正確的程式在執行中,很容易出現的、情理可容的異常狀況
可查異常雖然是異常狀況,但在一定程度上它的發生是可以預計的,而且一旦發生這種異常狀況,就必須採取某種方式進行處理。 除了RuntimeException及其子類以外,其他的Exception類及其子類都屬於可查異常。這種異常的特點是Java編譯器會檢查它,即當程式中可能出現這類異常,要麼用try-catch語句捕獲它,要麼用throws子句宣告丟擲它,否則編譯不會通過。

(2)不可查異常: 包括執行時異常(RuntimeException與其子類)和錯誤(Error)。


三、異常的處理

1、異常處理方式
在Java應用程式中,有兩種異常處理方式:丟擲異常或者捕捉異常。

(1)丟擲異常
當一個方法出現錯誤引發異常時,方法建立異常物件並交付執行時系統,異常物件中包含了異常型別和異常出現時的程式狀態等異常資訊。執行時系統負責尋找處置異常的程式碼並執行。 注意:對於執行時異常、錯誤或可查異常,Java技術所要求的異常處理方式有所不同。由於執行時異常的不可查性,為了更合理、更容易地實現應用程式,Java規定執行時異常將由Java執行時系統自動丟擲,允許應用程式忽略執行時異常。對於方法執行中可能出現的Error,當執行方法不捕捉時,Java允許該方法不做任何丟擲宣告。因為大多數Error異常屬於永遠不能被允許發生的狀況,也屬於合理的應用程式不該捕捉的異常。對於所有的可查異常,Java規定一個方法必須捕捉,或者宣告丟擲方法之外,即當一個方法選擇不捕捉可查異常時它必須宣告將異常丟擲

(2)捕獲異常
在方法丟擲異常之後,執行時系統將轉為尋找合適的異常處理器(exception handler)。潛在的異常處理器是異常發生時依次存留在呼叫棧中的方法的集合。當異常處理器所能處理的異常型別與方法丟擲的異常型別相符時,即為合適的異常處理器。執行時系統從發生異常的方法開始,依次回查呼叫棧中的方法直至找到含有合適異常處理器的方法並執行。當執行時系統遍歷呼叫棧而未找到合適的異常處理器,則執行時系統終止,同時也意味著Java程式的終止。


2、異常處理關鍵字

Java的異常處理通過5個關鍵字來實現:try、catch、throw、throws和finally。對這5個關鍵字解釋如下:
(1)try塊:用於捕獲異常。其後可接零個或多個catch塊,如果沒有catch塊,則必須跟一個finally塊。
(2)catch塊:用於處理try捕獲到的異常。
(3)finally塊:無論是否捕獲或處理異常,finally塊裡的語句都會被執行。當在try塊或catch塊中遇到return語句時,finally語句塊將在方法返回之前被執行。在以下4種特殊情況下,finally塊不會被執行:

  • 在finally語句塊中發生了異常。
  • 在前面的程式碼中用了System.exit()退出程式。
  • 程式所在的執行緒死亡。
  • 關閉CPU。

(4)throw語句:用於丟擲異常,用在catch塊捕獲到異常卻不想處理時丟擲異常。
(5)throws語句:用於宣告時可能會出現的異常,用在宣告方法上,此時異常由上層呼叫方法處理。


3、異常執行順序
try、catch、finally語句塊的執行順序如下:
(1)當try沒有捕獲到異常時:try語句塊中的語句逐一被執行,程式將跳過catch語句塊,執行finally語句塊和其後的語句;
(2)當try捕獲到異常,catch語句塊裡沒有處理此異常的情況
當try語句塊裡的某條語句出現異常時,而沒有處理此異常的catch語句塊時,此異常將會拋給JVM處理,finally語句塊裡的語句還是會被執行,但finally語句塊後的語句不會被執行。
(3)當try捕獲到異常,catch語句塊裡有處理此異常的情況
在try語句塊中是按照順序來執行的,當執行到某一條語句出現異常時,程式將跳到catch語句塊,並與catch語句塊逐一匹配,找到與之對應的處理程式,其他的catch語句塊將不會被執行,而try語句塊中,出現異常之後的語句也不會被執行,catch語句塊執行完後,執行finally語句塊裡的語句,最後執行finally語句塊後的語句。

執行流程如下圖所示:

4、丟擲異常實際操作
任何Java程式碼都可以丟擲異常,如:自己編寫的程式碼、來自Java開發環境包中程式碼,或者Java執行時系統。無論是誰都可以通過Java的throw語句丟擲異常。從方法中丟擲的任何異常都必須使用throws子句。
(1)使用throws丟擲異常
如果一個方法可能會出現異常,但沒有能力處理這種異常,可以在方法宣告處用throws子句來宣告丟擲異常。
throws語句用在方法定義時宣告該方法要丟擲的異常型別,如果丟擲的是Exception異常型別,則該方法被宣告為丟擲所有的異常。多個異常可使用逗號分割。throws語句的語法格式為:

public void methodName() throws Exception{
       //程式碼邏輯
    }

方法名後的throws Exception1,Exception2········ExceptionN 為宣告要丟擲的異常列表。當方法丟擲異常列表的異常時,方法將不對這些型別及其子類型別的異常作處理,而拋向呼叫該方法的方法,由他去處理。使用throws關鍵字將異常拋給呼叫者後,如果呼叫者不想處理該異常,可以繼續向上丟擲,但最終要有能夠處理該異常的呼叫者。
Throws丟擲異常的規則:

  • 如果是不可查異常(unchecked exception),即Error、RuntimeException或它們的子類,那麼可以不使用throws關鍵字來宣告要丟擲的異常,編譯仍能順利通過,但在執行時會被系統丟擲。
  • 必須宣告方法可丟擲的任何可查異常(checked exception)。即如果一個方法可能出現受可查異常,要麼用try-catch語句捕獲,要麼用throws子句宣告將它丟擲,否則會導致編譯錯誤。
  • 僅當丟擲了異常,該方法的呼叫者才必須處理或者重新丟擲該異常。當方法的呼叫者無力處理該異常的時候,應該繼續丟擲。
  • 呼叫方法必須遵循任何可查異常的處理和宣告規則。若覆蓋一個方法,則不能宣告與覆蓋方法不同的異常。宣告的任何異常必須是被覆蓋方法所宣告異常的同類或子類。

(2)使用throw丟擲異常
throw總是出現在函式體中,用來丟擲一個Throwable型別的異常。程式會在throw語句後立即終止,它後面的語句執行不到,然後在包含它的所有try塊中(可能在上層呼叫函式中)從裡向外尋找含有與其匹配的catch子句的try塊。
我們知道,異常是異常類的例項物件,我們可以建立異常類的例項物件通過throw語句丟擲。
該語句的語法格式為:throw new exceptionname;例如丟擲一個IOException類的異常物件:throw new IOException;
要注意的是,throw 丟擲的只能夠是可丟擲類Throwable 或者其子類的例項物件。
下面的操作是錯誤的:throw new String("exception");這是因為String 不是Throwable 類的子類。
如果丟擲了檢查異常,則還應該在方法頭部宣告方法可能丟擲的異常型別。該方法的呼叫者也必須檢查處理丟擲的異常。
如果所有方法都層層上拋獲取的異常,最終JVM會進行處理,處理也很簡單,就是列印異常訊息和堆疊資訊。如果丟擲的是Error或RuntimeException,則該方法的呼叫者可選擇處理該異常。


5、Throwable類中的常用方法
注意:catch關鍵字後面括號中的Exception型別的引數e。Exception就是try程式碼塊傳遞給catch程式碼塊的變數型別,e就是變數名。catch程式碼塊中語句"e.getMessage();"用於輸出錯誤性質。通常異常處理常用3個函式來獲取異常的有關資訊:

getCause():返回丟擲異常的原因。如果 cause 不存在或未知,則返回 null。
getMeage():返回異常的訊息資訊。
printStackTrace():物件的堆疊跟蹤輸出至錯誤輸出流,作為欄位 System.err 的值。

有時為了簡單會忽略掉catch語句後的程式碼,這樣try-catch語句就成了一種擺設,一旦程式在執行過程中出現了異常,就會忽略處理異常,而錯誤發生的原因很難查詢。


6、Java中常見異常
在Java中提供了一些異常用來描述經常發生的錯誤,對於這些異常,有的需要程式設計師進行捕獲處理或宣告丟擲,有的是由Java虛擬機器自動進行捕獲處理。
Java中常見的異常類:
(1) runtimeException子類:

1、 java.lang.ArrayIndexOutOfBoundsException //陣列索引越界異常。當對陣列的索引值為負數或大於等於陣列大小時丟擲。    

2、java.lang.ArithmeticException //算術條件異常。譬如:整數除零等。
    
3、java.lang.NullPointerException //空指標異常。當應用試圖在要求使用物件的地方使用了null時,丟擲該異常。譬如:呼叫null物件的例項方法、訪問null物件的屬性、計算null物件的長度、使用throw語句丟擲null等等
    
4、java.lang.ClassNotFoundException //找不到類異常。當應用試圖根據字串形式的類名構造類,而在遍歷CLASSPAH之後找不到對應名稱的class檔案時,丟擲該異常。

5、java.lang.NegativeArraySizeException //陣列長度為負異常

6、java.lang.ArrayStoreException //陣列中包含不相容的值丟擲的異常

7、java.lang.SecurityException //安全性異常

8、java.lang.IllegalArgumentException //非法引數異常

(2)IOException

IOException //操作輸入流和輸出流時可能出現的異常。

EOFException //檔案已結束異常

FileNotFoundException //檔案未找到異常

(3)其他

ClassCastException    //型別轉換異常類

ArrayStoreException  //陣列中包含不相容的值丟擲的異常

SQLException   //操作資料庫異常類

NoSuchFieldException   //欄位未找到異常

NoSuchMethodException   //方法未找到丟擲的異常

NumberFormatException    //字串轉換為數字丟擲的異常

StringIndexOutOfBoundsException //字串索引超出範圍丟擲的異常

IllegalAccessException  //不允許訪問某類異常

InstantiationException  //當應用程式試圖使用Class類中的newInstance()方法建立一個類的例項,而指定的類物件無法被例項化時,丟擲該異常

7、自定義異常
使用Java內建的異常類可以描述在程式設計時出現的大部分異常情況。除此之外,使用者還可以自定義異常。使用者自定義異常類,只需繼承Exception類即可。
在程式中使用自定義異常類,大體可分為以下幾個步驟:
(1)建立自定義異常類
一般會選擇繼承Exception和RuntimeException,如果不要求呼叫者一定要處理丟擲的異常,就繼承RuntimeException。

(2)丟擲自定義異常
在方法中通過throw關鍵字丟擲異常物件。

(3)捕獲自定義異常
如果在當前丟擲異常的方法中處理異常,可以使用try-catch語句捕獲並處理;否則在方法的宣告處通過throws關鍵字指明要丟擲給方法呼叫者的異常,繼續進行下一步操作。

(4)在出現異常方法的呼叫者中捕獲並處理異常。


四、異常安全

一段程式碼是異常安全的,即這段程式碼執行時的失敗不會產生有害後果,如記憶體洩露、儲存資料混淆、或無效的輸出。異常安全可分成以下5個層次:
1、失敗透明(failure transparency),也稱作不丟擲保證(no throw guarantee)
程式碼的執行保證能成功並滿足所有的約束條件,即使存在異常情況。如果出現了異常,將不會對外進一步丟擲該異常。(異常安全的最好的層次)

2、提交或回滾的語義(commit or rollback semantics),或稱作強異常安全(strong exception safety)或無變化保證(no-change guarantee)
執行可以失敗,但失敗的執行保證不會有負效應,因此所有涉及的資料都保持程式碼執行前的初始值。

3、基本異常安全(basic exception safety)
失敗執行的已執行的操作可能引起了副作用,但會保證狀態不變。所有儲存資料保持有效值,即使這些資料與異常發生前的值有所不同。

4、最小異常安全(minimal exception safety)也稱作無洩漏保證(no-leak guarantee)
失敗執行的已執行的操作可能在儲存資料中儲存了無效的值,但不會引起崩潰,資源不會洩漏。

5、異常不安全(no exception safety)
沒有保證(最差的異常安全層次)。

例如,考慮一個smart vector型別,如C++'s std::vector或Java's ArrayList。當一個數據項x插入vector v,必須實際增加x的值到vector的內部物件列表中並且修改vector的計數域以正確表示v中儲存了多少資料項;此時如果已有的儲存空間不夠大,就需要分配新的記憶體。記憶體分配可能會失敗並丟擲異常。
因此,vector資料型別如果是“失敗透明”保證將會非常困難甚至不可能實現。
但vector型別提供“強異常安全”保證卻是相當容易的;在這種情況下,x插入v或者成功,或者v保持不變。
如果vector型別僅提供“基本異常安全”保證,如果資料插入失敗,v可能包含也可能不包含x的值,但至少v的內部表示是一致的。
但如果vector資料型別是“最小異常安全”保證,v可能會是無效的,例如v的計數域被增加了,但x並未實際插入,使得內部狀態不一致。
對於“異常不安全”的實現,程式可能會崩潰,例如寫入資料到無效的記憶體。

通常至少需要基本異常安全。失敗透明是難於實現的,特別是在編寫庫函式時,因為對應用程式的複雜知識缺少獲知。


五、異常在實際應用中的經驗與總結

參考資料
(1) https://www.jianshu.com/p/872844d995c4
(2) http://c.biancheng.net/view/1043.html (5個異常處理關鍵字)
(3) https://zh.wikipedia.org/wiki/%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86 (維基百科)
(4) https://www.cnblogs.com/Qian123/p/5715402.html
(5) https://blog.csdn.net/hguisu/article/details/6155636