Java中應用程序在非正常的情況下停止運行主要包含兩種方式: Error 和 Exception ,像我們熟知的 OutOfMemoryError 和 IndexOutOfBoundsException 等。在日常的開發過程中 Error 我們是不用處理的,一旦 Error 發生必然證明應用程序代碼上出現了問題,這個時候我們只能是修改代碼。而 Exception 則是在程序運行的過程中可以進行捕獲並處理的。接下來的所有討論均會以 Exception 為主。
異常的繼承圖
所有的異常均繼承在 Exception 類,而在其子類中又分為運行時異常RuntimeException 和 其他的異常類。所謂的運行時異常指的是在運行的時候所拋出的異常,例如:IndexOutOfBoundsException 就是 RuntimeException 的子類,訪問數組下標范圍外的元素,在代碼的編譯時期是無法被檢測到的,只有在運行的時候才會拋出這樣的異常。其他的異常則是 Exception 的直接子類,這些異常也可以稱為編譯時異常,編譯時異常雖然是運行時被拋出的,但是在代碼編寫的過程中就會知道某些代碼可能會拋出異常,並需要對其作出反應:處理或者是繼續向上一層調用棧拋出。他們的主要區別可以用一句話來總結:編譯時期可以發現並且需要對其作出反應的異常為編譯時異常,在編譯時期不能發現的異常叫運行時異常。當運行時異常發生的時候,也意味着我們是需要調整我們的代碼的。在下面討論 throws 關鍵字的時候會詳細說明。
throw 異常的可拋性
所有的異常均可以使用 throw 關鍵字主動拋出,但 throw 並不能拋出一切東西,例如我們無法使用 throw 拋出一個數組或者是一個 Interger 實例。異常具有可拋性是因為 Exception 的父類是 Throwable ,只有繼承它,才具有可拋性。這一點作為了解就可以了,在Java內部已經為我們定義了很多的異常,如果感覺內置的異常無法滿足要求,我們也可以通過繼承自`Exception 或 RuntimeExcetpion 來擴展異常。
throws 異常聲明
上面我們講過了異常的分類,包含了編譯時異常和運行時異常。但是在Java中異常是需要聲明的,當我們使用 throw 進行主動拋出異常時,需要在方法的后面加上 throws 關鍵字和異常的名字進行聲明,否則代碼無法正常編譯。
public void getName() throws CustomException1,CustomException2 .... { //方法體 }
在這種情況下,如果別人調用我們的代碼或者是我們調用其他人有類似聲明的代碼(例如:SimpleDateFormat 的 Parse方法)時,調用者就要對其作出反應,處理或是在調用者的方法體上繼續聲明,從而告訴上一層調動棧去處理,如果所有的調用棧均不處理,那么最后只能由JVM進行兜底。對於使用throws關鍵字進行聲明的異常均為編譯時異常,運行時異常在編譯時無法感知,所以不需要進行異常聲明。另在異常聲明的時,聲明的異常類型可以是拋出異常的名字或者是該異常父類的名字。
public void getName() throws Exception{
throw new CustomException1("自定義Exception異常");
}
異常的捕獲
當調用異常聲明的方法時,需要對其作出反應,Java提供了 try .... catch ....finally
關鍵字,try 中的代碼塊主要是用於作為異常檢測,catch 用於捕獲檢測到的異常並處理,而 finally 則是必須執行的代碼塊。下面我看看一個例子
// 方法的聲明
public void getName() throws CustomException { // 方法體 }
// 調用體
try { getName(); }
catch(CustomException ex){ // 處理異常 }
finally{ // 必須執行的代碼 }
代碼 finally 體內的代碼塊,不管異常能不能被捕獲到,都是會執行的,通常用於做資源的釋放操作。上述的代碼只是一個簡單捕獲的例子,如果 getName 方法拋出多個異常 throws CustomException1, CusomException2 , Exception ,那又該如果捕獲呢?在Java中是支持多個catch 捕獲異常的,當異常的種類是同級時,多 catch 的編寫順序可以使任意的,但如果異常的聲明中包含非同級時( 例如包含其父類異常 ) 編寫多 catch 異常捕獲的時候,需要在最后寫父類捕獲代碼。
try { getName(); }
catch(CustomException2 ex){ // 處理異常 }
catch(CustomException1 ex){ // 處理異常 }
catch(Exception ex){ // 處理異常 }
finally{ //必須執行的代碼 }
方法重寫時的異常處理
由於Java的編譯時異常需要進行聲明,Java又是面向對象編程語言,存在繼承和多態特性,隨之而來的就會產生對存在這種關系的方法體異常的聲明該如何定義?下面我們直接來說結論
- 當父類中對方法進行異常聲明,那么子類對該方法進行覆蓋時,可以進行聲明也可以不進行聲明,當進行聲明的時候,聲明的異常在繼承關系上只能是該異常或者是它的子類。
- 當父類中對方法沒有進行異常聲明時,那么子類對該方法進行覆蓋時,也不可以進行異常聲明,如果子類中的方法調用了帶有異常聲明的方法,由於這個限制,只能在方法中進行 try....catch 處理
下面我們一起看下幾個場景:
父類中的方法聲明異常
public class Demo {
public static void main(String[] args) throws CustomException {
Person p = new Student();
p.GetName();
}
}
class Person{
public String GetName() throws CustomException { throw new CustomException("自定義異常"); }
}
class Student extends Person {
public String GetName() throws CustomException2 , CustomException
{
if(true) { throw new CustomException("自定義異常"); }
else { throw new CustomException2("自定義異常2"); }
}
}
class CustomException extends Exception {
public CustomException() {}
public CustomException(String message) { super(message); }
}
class CustomException2 extends CustomException {
public CustomException2() {}
public CustomException2(String message) { super(message); }
}
或
class Student extends Person {
public String GetName() { // 方法體 }
}
父類中的方法未聲明異常
class Person{
public String GetName() { // 方法體 }
}
class Student extends Person {
public String GetName() {
try { // 調用帶有異常聲明的方法 }
catch( ... ) { // 處理異常 }
}
}
我們來理解一下為什么會有這樣的限制?其根源即使多態性,編譯看父類,運行是子類,當代碼編譯的時候,編譯器解析的是父類的方法,查看的是父類的異常聲明。如果子類中的異常聲明高於父類或者和父類同級,試想如果父類是CustomException,子類的聲明是 Exception,那么在編譯的時候,編譯器會提示需要處理異常 CustomException,但是在運行的時候,拋出的確是Exception,這樣對於異常的捕捉就能造成很大的困擾,以至於達不到我們的預期。
總結
在本節中我們講了Java中的異常,包含異常的分類,自定義異常,異常聲明和捕捉以及在多態調用中異常的聲明。在代碼開發的過程中,我們應該在不同的場景中選擇不同的異常使用。但是總體來看我們會使用運行時類型的異常會比較多,因為交付的軟件預期內的問題我已經解決了,在軟件運行的過程中產生的異常往往不在預期內,而且一旦發生就需要我們調整代碼。