1. 程式人生 > >深入理解Java中異常體系

深入理解Java中異常體系

任何程式都追求正確有效的執行,除了保證我們程式碼儘可能的少出錯之外,我們還要考慮如何有效的處理異常,一個良好的異常框架對於系統來說是至關重要的。最近在給公司寫採集框架的時候系統的瞭解一邊,收穫頗多,特此記錄相關的理論。

1 .異常體系簡介:

異常是指由於各種不期而至的情況,導致程式中斷執行的一種指令流,如:檔案找不到、非法引數、網路超時等。為了保證正序正常執行,在設計程式時必須考慮到各種異常情況,並正確的對異常進行處理。異常也是一種物件,java當中定義了許多異常類,並且定義了基類java.lang.Throwable作為所有異常的超類。Java語言設計者將異常劃分為兩類:Error和Exception,其體系結構大致如下圖所示:

Throwable:有兩個重要的子類:Exception(異常)和Error(錯誤),兩者都包含了大量的異常處理類。

1、Error(錯誤):是程式中無法處理的錯誤,表示執行應用程式中出現了嚴重的錯誤。此類錯誤一般表示程式碼執行時JVM出現問題。通常有Virtual MachineError(虛擬機器執行錯誤)、NoClassDefFoundError(類定義錯誤)等。比如說當jvm耗完可用記憶體時,將出現OutOfMemoryError。此類錯誤發生時,JVM將終止執行緒。

這些錯誤是不可查的,非程式碼性錯誤。因此,當此類錯誤發生時,應用不應該去處理此類錯誤。

2、Exception(異常):程式本身可以捕獲並且可以處理的異常。

Exception這種異常又分為兩類:執行時異常和編譯異常。

1、執行時異常(不受檢異常):RuntimeException類極其子類表示JVM在執行期間可能出現的錯誤。比如說試圖使用空值物件的引用(NullPointerException)、陣列下標越界(ArrayIndexOutBoundException)。此類異常屬於不可查異常,一般是由程式邏輯錯誤引起的,在程式中可以選擇捕獲處理,也可以不處理。

2、編譯異常(受檢異常):Exception中除RuntimeException極其子類之外的異常。如果程式中出現此類異常,比如說IOException,必須對該異常進行處理,否則編譯不通過。在程式中,通常不會自定義該類異常,而是直接使用系統提供的異常類。

可查異常與不可查異常:java的所有異常可以分為可查異常(checked exception)和不可查異常(unchecked exception)。

1、可查異常:編譯器要求必須處理的異常。正確的程式在執行過程中,經常容易出現的、符合預期的異常情況。一旦發生此類異常,就必須採用某種方式進行處理。除RuntimeException及其子類外,其他的Exception異常都屬於可查異常。編譯器會檢查此類異常,也就是說當編譯器檢查到應用中的某處可能會此類異常時,將會提示你處理本異常——要麼使用try-catch捕獲,要麼使用throws語句丟擲,否則編譯不通過。

2、不可查異常:編譯器不會進行檢查並且不要求必須處理的異常,也就說當程式中出現此類異常時,即使我們沒有try-catch捕獲它,也沒有使用throws丟擲該異常,編譯也會正常通過。該類異常包括執行時異常(RuntimeException極其子類)和錯誤(Error)。

2.異常處理流程:

在java應用中,異常的處理機制分為丟擲異常和捕獲異常。

丟擲異常:當一個方法出現錯誤而引發異常時,該方法會將該異常型別以及異常出現時的程式狀態資訊封裝為異常物件,並交給本應用。執行時,該應用將尋找處理異常的程式碼並執行。任何程式碼都可以通過throw關鍵詞丟擲異常,比如java原始碼丟擲異常、自己編寫的程式碼丟擲異常等。

捕獲異常:一旦方法丟擲異常,系統自動根據該異常物件尋找合適異常處理器(Exception Handler)來處理該異常。所謂合適型別的異常處理器指的是異常物件型別和異常處理器型別一致。

對於不同的異常,java採用不同的異常處理方式:

1、執行異常將由系統自動丟擲,應用本身可以選擇處理或者忽略該異常。

2、對於方法中產生的Error,該異常一旦發生JVM將自行處理該異常,因此java允許應用不丟擲此類異常。

3、對於所有的可查異常,必須進行捕獲或者丟擲該方法之外交給上層處理。也就是當一個方法存在異常時,要麼使用try-catch捕獲,要麼使用該方法使用throws將該異常拋呼叫該方法的上層呼叫者。

2.1捕獲異常

1、try-catch語句

try {

//可能產生的異常的程式碼區,也成為監控區

}catch (ExceptionType1 e) {

//捕獲並處理try丟擲異常型別為ExceptionType1的異常

}catch(ExceptionType2 e) {

//捕獲並處理try丟擲異常型別為ExceptionType2的異常

}

監控區一旦發生異常,則會根據當前執行時的資訊建立異常物件,並將該異常物件丟擲監控區,同時

系統根據該異常物件依次匹配catch子句,若匹配成功(丟擲的異常物件的型別和catch子句的異常類的型別或者是該異常類的子類的型別一致),則執行其中catch程式碼塊中的異常處理程式碼,一旦處理結束,那就意味著整個try-catch結束。含有多個catch子句,一旦其中一個catch子句與丟擲的異常物件型別一致時,其他catch子句將不再有匹配異常物件的機會。

2、try-catch-finally

try {

//可能產生的異常的程式碼區

}catch (ExceptionType1 e) {

//捕獲並處理try丟擲異常型別為ExceptionType1的異常

}catch (ExceptionType2 e){

//捕獲並處理try丟擲異常型別為ExceptionType2的異常

}finally{

//無論是出現異常,finally塊中的程式碼都將被執行

}

3、try-catch-finally程式碼塊的執行順序:

A)try沒有捕獲異常時,try程式碼塊中的語句依次被執行,跳過catch。如果存在finally則執行finally程式碼塊,否則執行後續程式碼。

      B)try捕獲到異常時,如果沒有與之匹配的catch子句,則該異常交給JVM處理。如果存在finally,則其中的程式碼仍然被執行,但是finally之後的程式碼不會被執行。

      C)try捕獲到異常時,如果存在與之匹配的catch,則跳到該catch程式碼塊執行處理。如果存在finally則執行finally程式碼塊,執行完finally程式碼塊之後繼續執行後續程式碼;否則直接執行後續程式碼。另外注意,try程式碼塊出現異常之後的程式碼不會被執行。(見下圖:)

4、總結

try程式碼塊:用於捕獲異常。其後可以接零個或者多個catch塊。如果沒有catch塊,後必須跟finally塊,來完成資源釋放等操作,另外建議不要在finally中使用return,不用嘗試通過catch來控制程式碼流程。

catch程式碼塊:用於捕獲異常,並在其中處理異常。

finally程式碼塊:無論是否捕獲異常,finally程式碼總會被執行。如果try程式碼塊或者catch程式碼塊中有return語句時,finally程式碼塊將在方法返回前被執行。注意以下幾種情況,finally程式碼塊不會被執行:

1、 在前邊的程式碼中使用System.exit()退出應用。

2、 程式所在的執行緒死亡或者cpu關閉

3、 如果在finally程式碼塊中的操作又產生異常,則該finally程式碼塊不能完全執行結束,同時該異常會覆蓋前邊丟擲的異常。

2.2丟擲異常

1、throws丟擲異常

如果一個方法可能丟擲異常,但是沒有能力處理該異常或者需要通過該異常向上層彙報處理結果,可以在方法宣告時使用throws來丟擲異常。這就相當於計算機硬體發生損壞,但是計算機本身無法處理,就將該異常交給維修人員來處理。

publicmethodName throws Exception1,Exception2….(params){}

其中Exception1,Exception2…為異常列表一旦該方法中某行程式碼丟擲異常,則該異常將由呼叫該方法的上層方法處理。如果上層方法無法處理,可以繼續將該異常向上層拋。

2、throw丟擲異常

在方法內,用throw來丟擲一個Throwable型別的異常。一旦遇到到throw語句,後面的程式碼將不被執行。然後,便是進行異常處理——包含該異常的try-catch最終處理,也可以向上層丟擲。注意我們只能丟擲Throwable類和其子類的物件。

throw newExceptionType;

比如我們可以丟擲:throw new Exception();

也有時候我們也需要在catch中丟擲異常,這也是允許的,比如說:

Try{

//可能會發生異常的程式碼

}catch(Exceptione){

      throw newException(e);

}

3、異常關係鏈

在實際開發過程中經常在捕獲一個異常之後丟擲另外一個異常,並且我們希望在新的異常物件中儲存原始異常物件的資訊,實際上就是異常傳遞,即把底層的異常物件傳給上層,一級一級,逐層丟擲。當程式捕獲了一個底層的異常,而在catch處理異常的時候選擇將該異常拋給上層…這樣異常的原因就會逐層傳遞,形成一個由低到高的異常鏈。但是異常鏈在實際應用中一般不建議使用,同時異常鏈每次都需要就將原始的異常物件封裝為新的異常物件,消耗大量資源。現在(jdk 1.4之後)所有的Throwable的子類構造中都可以接受一個cause物件,這個cause也就是原始的異常物件。

下面是一個不錯的例子:

/*

*高層異常

*/

classHighLevelExceptionextends Exception{

public HighLevelException(Throwable cause) {

super(cause);

}

}

/*

*中層異常

*/

classMiddleLevelExceptionextends Exception{

public MiddleLevelException(Throwable cause) {

super(cause);

}

}

/*

*底層異常

*/

classLowLevelExceptionextends Exception{

}

publicclass TestException {

publicvoid highLevelAccess()throws HighLevelException{

try {

middleLevelAccess();

}catch (Exception e) {

thrownew HighLevelException(e);

}

}

publicvoid middleLevelAccess()throws MiddleLevelException{

try {

lowLevelAccess();

}catch (Exception e) {

thrownew MiddleLevelException(e);

}

}

publicvoid lowLevelAccess()throws LowLevelException {

thrownew LowLevelException();

}

publicstaticvoid main(String[] args) {

/*

 * lowlevelAccess()將異常物件拋給middleLevelAccess(),而

 * middleLevelAccess()又將異常物件拋給highLevelAccess(),

 *也就是底層的異常物件一層層傳遞給高層。最終在在高層可以獲得底層的異常物件。

 */

try {

new TestException().highLevelAccess();

}catch (HighLevelException e) {

e.printStackTrace();

System.out.println(e.getCause());

}

}

}

4、異常轉譯

異常轉義就是將一種型別的異常轉成另一種型別的異常,然後再丟擲異常。之所以要進行轉譯,是為了更準確的描述異常。就我個人而言,我更喜歡稱之為異常型別轉換。在實際應用中,為了構建自己的日誌系統,經常需要把系統的一些異常資訊描述成我們想要的異常資訊,就可以使用異常轉譯。異常轉譯針對所有Throwable類的子類而言,其子型別都可以相互轉換。

通常而言,更為合理的轉換方式是:

1、 Error——>Exception

2、 Error——>RuntimeException

3、 Exception——>RuntimeException,

在下面的程式碼中,我們自定義了MyException異常類,然後我們將IOException型別的異常轉為MyException型別的異常,最後丟擲。

class MyExceptionextends Exception {

public MyException(String msg, Throwable e) {

super(msg, e);

}

}

publicclass Demo {

publicstaticvoid main(String[] args)throws MyException {

Filefile =new File("H:/test.txt");

if (file.exists())

try {

file.createNewFile();

}catch (IOException e) {

thrownew MyException("檔案建立失敗!", e);

}

}

}

5、Throwable類中常用的方法

像catch(Exception e)中的Exception就是異常的變數型別,e則是形參。通常在進行異常輸出時有如下幾個方法可用:

e.getCause():返回丟擲異常的原因。

e.getMessage():返回異常資訊。

e.printStackTrace():發生異常時,跟蹤堆疊資訊並輸出。

6、 常異總結

此部分可以api文件中進行查閱,這裡僅做參考。

常見異常:

java.lang.IllegalAccessError:違法訪問錯誤。當一個應用試圖訪問、修改某個類的域(Field)或者呼叫其方法,但是又違反域或方法的可見性宣告,則丟擲該異常。

java.lang.InstantiationError:例項化錯誤。當一個應用試圖通過Java的new操作符構造一個抽象類或者介面時丟擲該異常.

java.lang.OutOfMemoryError:記憶體不足錯誤。當可用記憶體不足以讓Java虛擬機器分配給一個物件時丟擲該錯誤。
java.lang.StackOverflowError:堆疊溢位錯誤。當一個應用遞迴呼叫的層次太深而導致堆疊溢位或者陷入死迴圈時丟擲該錯誤。

java.lang.ClassCastException:類造型異常。假設有類A和B(A不是B的父類或子類),O是A的例項,那麼當強制將O構造為類B的例項時丟擲該異常。該異常經常被稱為強制型別轉換異常。
java.lang.ClassNotFoundException:找不到類異常。當應用試圖根據字串形式的類名構造類,而在遍歷CLASSPAH之後找不到對應名稱的class檔案時,丟擲該異常。

java.lang.ArithmeticException:算術條件異常。譬如:整數除零等。
java.lang.ArrayIndexOutOfBoundsException:陣列索引越界異常。當對陣列的索引值為負數或大於等於陣列大小時丟擲。

java.lang.IndexOutOfBoundsException:索引越界異常。當訪問某個序列的索引值小於0或大於等於序列大小時,丟擲該異常。
java.lang.InstantiationException:例項化異常。當試圖通過newInstance()方法建立某個類的例項,而該類是一個抽象類或介面時,丟擲該異常。

java.lang.NoSuchFieldException:屬性不存在異常。當訪問某個類的不存在的屬性時丟擲該異常。
java.lang.NoSuchMethodException:方法不存在異常。當訪問某個類的不存在的方法時丟擲該異常。
java.lang.NullPointerException:空指標異常。當應用試圖在要求使用物件的地方使用了null時,丟擲該異常。譬如:呼叫null物件的例項方法、訪問null物件的屬性、計算null物件的長度、使用throw語句丟擲null等等。
java.lang.NumberFormatException:數字格式異常。當試圖將一個String轉換為指定的數字型別,而該字串確不滿足數字型別要求的格式時,丟擲該異常。

java.lang.StringIndexOutOfBoundsException:字串索引越界異常。當使用索引值訪問某個字串中的字元,而該索引值小於0或大於等於序列大小時,丟擲該異常。

其他異常:
java.lang.AbstractMethodError:抽象方法錯誤。當應用試圖呼叫抽象方法時丟擲。
java.lang.AssertionError:斷言錯。用來指示一個斷言失敗的情況。
java.lang.ClassCircularityError:類迴圈依賴錯誤。在初始化一個類時,若檢測到類之間迴圈依賴則丟擲該異常。
java.lang.ClassFormatError:類格式錯誤。當Java虛擬機器試圖從一個檔案中讀取Java類,而檢測到該檔案的內容不符合類的有效格式時丟擲。
java.lang.Error:錯誤。是所有錯誤的基類,用於標識嚴重的程式執行問題。這些問題通常描述一些不應被應用程式捕獲的反常情況。
java.lang.ExceptionInInitializerError:初始化程式錯誤。當執行一個類的靜態初始化程式的過程中,發生了異常時丟擲。靜態初始化程式是指直接包含於類中的static語句段。
java.lang.IncompatibleClassChangeError:不相容的類變化錯誤。當正在執行的方法所依賴的類定義發生了不相容的改變時,丟擲該異常。一般在修改了應用中的某些類的宣告定義而沒有對整個應用重新編譯而直接執行的情況下,容易引發該錯誤。
java.lang.InternalError:內部錯誤。用於指示Java虛擬機發生了內部錯誤。
java.lang.LinkageError:連結錯誤。該錯誤及其所有子類指示某個類依賴於另外一些類,在該類編譯之後,被依賴的類改變了其類定義而沒有重新編譯所有的類,進而引發錯誤的情況。
java.lang.NoClassDefFoundError:未找到類定義錯誤。當Java虛擬機器或者類裝載器試圖例項化某個類,而找不到該類的定義時丟擲該錯誤。
java.lang.NoSuchFieldError:域不存在錯誤。當應用試圖訪問或者修改某類的某個域,而該類的定義中沒有該域的定義時丟擲該錯誤。
java.lang.NoSuchMethodError:方法不存在錯誤。當應用試圖呼叫某類的某個方法,而該類的定義中沒有該方法的定義時丟擲該錯誤。
java.lang.ThreadDeath:執行緒結束。當呼叫Thread類的stop方法時丟擲該錯誤,用於指示執行緒結束。
java.lang.UnknownError:未知錯誤。用於指示Java虛擬機發生了未知嚴重錯誤的情況。
java.lang.UnsatisfiedLinkError:未滿足的連結錯誤。當Java虛擬機器未找到某個類的宣告為native方法的本機語言定義時丟擲。
java.lang.UnsupportedClassVersionError:不支援的類版本錯誤。當Java虛擬機器試圖從讀取某個類檔案,但是發現該檔案的主、次版本號不被當前Java虛擬機器支援的時候,丟擲該錯誤。
java.lang.VerifyError:驗證錯誤。當驗證器檢測到某個類檔案中存在內部不相容或者安全問題時丟擲該錯誤。
java.lang.VirtualMachineError:虛擬機器錯誤。用於指示虛擬機器被破壞或者繼續執行操作所需的資源不足的情況。
java.lang.ArrayStoreException:陣列儲存異常。當向陣列中存放非陣列宣告型別物件時丟擲。
java.lang.CloneNotSupportedException:不支援克隆異常。當沒有實現Cloneable介面或者不支援克隆方法時,呼叫其clone()方法則丟擲該異常。
java.lang.EnumConstantNotPresentException:列舉常量不存在異常。當應用試圖通過名稱和列舉型別訪問一個列舉物件,但該列舉物件並不包含常量時,丟擲該異常。
java.lang.Exception:根異常。用以描述應用程式希望捕獲的情況。
java.lang.IllegalAccessException:違法的訪問異常。當應用試圖通過反射方式建立某個類的例項、訪問該類屬性、呼叫該類方法,而當時又無法訪問類的、屬性的、方法的或構造方法的定義時丟擲該異常。
java.lang.IllegalMonitorStateException:違法的監控狀態異常。當某個執行緒試圖等待一個自己並不擁有的物件(O)的監控器或者通知其他執行緒等待該物件(O)的監控器時,丟擲該異常。
java.lang.IllegalStateException:違法的狀態異常。當在Java環境和應用尚未處於某個方法的合法呼叫狀態,而呼叫了該方法時,丟擲該異常。
java.lang.IllegalThreadStateException:違法的執行緒狀態異常。當縣城尚未處於某個方法的合法呼叫狀態,而呼叫了該方法時,丟擲異常。
java.lang.InterruptedException:被中止異常。當某個執行緒處於長時間的等待、休眠或其他暫停狀態,而此時其他的執行緒通過Thread的interrupt方法終止該執行緒時丟擲該異常。
java.lang.NegativeArraySizeException:陣列大小為負值異常。當使用負數大小值建立陣列時丟擲該異常。
java.lang.SecurityException:安全異常。由安全管理器丟擲,用於指示違反安全情況的異常。
java.lang.TypeNotPresentException:型別不存在異常。