Java中的異常體系
吹逼兩小時,程式碼五分鐘。平常寫程式碼中異常無處不在,早就練就了我們try{...} actch(){...}finally{...}
一把梭的深厚功力。畢竟它看起來真的很簡單,我們用著也蠻順心,但是你真的瞭解它嗎,真的不會出錯嗎?
Java異常體系
下面是Java中的異常繼承圖:
Throwable
是整個異常體系中的頂級父類,它擁有兩個子類,分別是Error
和Exception
。
Error
是由機器底層丟擲的錯誤,我們無法處理的,比如OutOfMemoryError
,當遇到這類錯誤時,JVM
會直接終止程序,應用終止。此類異常我們不要去捕獲,因為捕獲了也處理不了。
Exception
是程式可以處理的錯誤,主要分為執行時異常,和非執行時異常。或者叫做受檢異常和非受檢異常。
執行時異常都是RuntimeException
及其子類,比如,NullPointException
、ArrayIndexOutOfBoundsException
等等,這類的異常屬於非受檢異常(UnChecked
),我們可以對其捕獲處理,也可以不處理,而我們一般也不會做處理的,因為這類錯誤通常是我們邏輯錯誤所導致的,我們應該儘量避免此類Bug
。
而非執行時異常,比如IOException
、EOFException
等等所有非RuntimeException
及其子類,都是非執行時異常,也就是受檢異常(Checked
異常處理
那麼我們該怎麼去處理異常呢,這時候自然而然會想到try-catch-finally
,我們來下面的程式碼:
public static void main(String[] args) {
System.out.println(test1());
}
public static boolean test1() {
boolean flag = true;
try {
flag = test2();
} catch (Exception e) {
System.out.println("[test1] catch exception result=" + flag);
flag = false;
throw e;
} finally {
System.out.println("[test1] finally result=" + flag);
return flag;
}
}
public static boolean test2() throws Exception {
boolean flag = true;
try {
// 分別呼叫test3()和test4()
flag = test4();
if (!flag) {
return false;
}
System.out.println("[test2] result=" + flag);
return flag;
} catch (Exception e) {
System.out.println("[test2] catch exception result=" + flag);
flag = false;
throw e;
} finally {
System.out.println("[test2] finally result=" + flag);
return flag;
}
}
public static boolean test3() throws Exception {
boolean flag = true;
try {
System.out.println("[test3] result=" + flag);
return true;
} catch (Exception e) {
System.out.println("[test3] catch exception result=" + flag);
flag = false;
throw e;
} finally {
System.out.println("[test3] finally result=" + flag);
return flag;
}
}
public static boolean test4() throws Exception {
boolean flag = true;
try {
int a = 2 / 0;
System.out.println("[test4] result=" + flag);
return true;
} catch (Exception e) {
System.out.println("[test4] catch exception result=" + flag);
flag = false;
throw e;
} finally {
System.out.println("[test4] finally result=" + flag);
return flag;
}
}
當其中test2()
方法分別呼叫test3()
和teset4()
,列印順序會是怎麼樣的呢,不如先思考一下。
呼叫test3()
時,列印結果:
[test3] result=true
[test3] finally result=true
[test2] result=true
[test2] finally result=true
[test1] finally result=true
true
呼叫test4()
時,列印結果:
[test4] catch exception result=true
[test4] finally result=false
[test2] finally result=false
[test1] finally result=false
false
不知道這段程式碼的實際執行結果和你所想的有沒有出入,眾所周知的是,當try{···}
塊中的程式碼執行未發生異常,賊執行finally{...}
塊中的程式碼,如果執行出現異常,則會先執行catch(){...}
中的程式碼,再執行finally{...}
中的程式碼。但是當這些程式碼塊中出現return
、throw
則會發生變化。
當try{···}塊或者catch(){…}塊中有return、throw時,在return或者throw執行前回優先執行finally{…}程式碼塊中的內容
特別提醒,
finally
中禁止使用return
,這裡使用,只是為了演示用。
所以當finally
中有throw
或者return
時,它會覆蓋try
塊中和catch
塊中的throw
以及return
,從而出現異常遮蔽現象,比如下面這段程式碼:
public static void main(String[] args) {
try {
int i = 1 / 0;
} catch (Exception e) {
throw new RuntimeException("catch");
} finally {
throw new RuntimeException("finally");
}
}
它將打印出:
Exception in thread "main" java.lang.RuntimeException: finally
at top.felixu.exception.ExceptionDemo1.main(ExceptionDemo1.java:14)
catch
塊中的異常就被覆蓋了,這種現象還是有可能出現在try-catch-finally
捕獲處理流相關操作中,在finally
中手動關閉流出現異常時,但是在JDK7
版本以後,try-with-resource
可以幫我們自動關閉流了。在JDK7
中,所有的IO
類都實現了AutoCloseable
介面,並且需要實現其中的close()
函式,資源釋放過程需要在該函式中完成。那麼,編譯器在編譯時,會自動新增finally
程式碼塊,並將close()
函式中的資源釋放程式碼加入finally
程式碼塊中,從而提高程式碼可讀性。這裡就不具體介紹了。
異常處理約定
- 對於非受檢異常,我們不要去捕獲處理,而是通過測試和
review
程式碼來規避此類問題。 - 異常不要用來做流程控制,條件控制,因為異常處理的效率比分支處理要低。
- 對大塊程式碼的
try-catch
是不負責的行為,我們要區分穩定程式碼已經不穩定程式碼。 - 捕獲異常是為了處理異常,如果捕獲了什麼都不處理,不如不捕獲,將其拋給其上層呼叫者。而最上層的呼叫者必須處理,防止使用者看到無法理解的異常資訊。
try
塊中有事物程式碼,則catch
到異常要手動回滾,或者丟擲異常,讓AOP
框架來回滾,否則會吞掉異常,導致異常卻未回滾。- 不能在
finally
中使用return
。 - 捕獲異常必須與所丟擲的異常匹配,或者所捕獲的異常是丟擲異常的父類。