Java基礎10——深入理解Java異常
深入理解Java異常
為什麼使用異常?
使用異常機制它能夠降低錯誤處理程式碼的複雜度,如果不使用異常,那麼就必須檢查特定的錯誤,並在程式中的許多地方去處理它,而如果使用異常,那就不必在方法呼叫處進行檢查,因為異常機制將保證能夠捕獲這個錯誤,並且,只需在一個地方處理錯誤,即所謂的異常處理程式中。這種方式不僅節約程式碼,而且把“概述在正常執行過程中做什麼事”的程式碼和“出了問題怎麼辦”的程式碼相分離。總之,與以前的錯誤處理方法相比,異常機制使程式碼的閱讀、編寫和除錯工作更加井井有條。(摘自《Think in java 》)。chessy老師的部落格
在《Think in java》中是這樣定義異常的:異常情形是指阻止當前方法或者作用域繼續執行的問題。在這裡一定要明確一點:異常程式碼某種程度的錯誤,儘管Java有異常處理機制,但是我們不能以“正常”的眼光來看待異常,異常處理機制的原因就是告訴你:這裡可能會或者已經產生了錯誤,您的程式出現了不正常的情況,可能會導致程式失敗!
那麼什麼時候才會出現異常呢?只有在你當前的環境下程式無法正常執行下去,也就是說程式已經無法來正確解決問題了,這時它所就會從當前環境中跳出,並丟擲異常。丟擲異常後,它首先會做幾件事。首先,它會使用new建立一個異常物件,然後在產生異常的位置終止程式,並且從當前環境中彈出對異常物件的引用,這時。異常處理機制就會接管程式,並開始尋找一個恰當的地方來繼續執行程式,這個恰當的地方就是異常處理程式,它的任務就是將程式從錯誤狀態恢復,以使程式要麼換一種方法執行,要麼繼續執行下去
總的來說異常處理機制就是當程式發生異常時,它強制終止程式執行,記錄異常資訊並將這些資訊反饋給我們,由我們來確定是否處理異常。
異常體系
從上面這幅圖可以看出,Throwable是java語言中所有錯誤和異常的超類(萬物即可拋)。它有兩個子類:Error、Exception。
其中 Error 為錯誤,是程式無法處理的,如 OutOfMemoryError、ThreadDeath 等,出現這種情況你唯一能做的就是聽之任之,交由 JVM 來處理,不過 JVM 在大多數情況下會選擇終止執行緒。
常見的Error主要包括:
OutOfMemoryError:記憶體溢位錯誤
StackOverflowError:棧溢位錯誤
VirtualMachineError:虛擬機器錯誤
NoClassDefFoundError:找不到類錯誤
而 Exception 是程式可以處理的異常。它又分為兩種 CheckedException(受撿異常),一種是UncheckedException(不受檢異常)。其中CheckException發生在編譯階段,必須要使用try…catch(或者throws)否則編譯不通過。而UncheckedException發生在執行期,具有不確定性,主要是由於程式的邏輯問題所引起的,難以排查,我們一般都需要縱觀全域性才能夠發現這類的異常錯誤,所以在程式設計中我們需要認真考慮,好好寫程式碼,儘量處理異常,即使產生了異常,也能儘量保證程式朝著有利方向發展。
所以:對於可恢復的條件使用被檢查的異常(CheckedException),對於程式錯誤(言外之意不可恢復,大錯已經釀成)使用執行時異常(RuntimeException)。
ArithmeticException(算術異常)
ClassCastException (類轉換異常)
IllegalArgumentException (非法引數異常) IndexOutOfBoundsException (下標越界異常)
NullPointerException (空指標異常)
SecurityException (安全異常)
CheckedException&UncheckedException
非檢查異常(unchecked exception):Error 和 RuntimeException 以及他們的子類。javac在編譯時,不會提示和發現這樣的異常,不要求在程式處理這些異常。所以如果願意,我們可以編寫程式碼處理(使用try…catch…finally)這樣的異常,也可以不處理。
對於這些異常,我們應該修正程式碼,而不是去通過異常處理器處理 。這樣的異常發生的原因多半是程式碼寫的有問題。如除0錯誤ArithmeticException,錯誤的強制型別轉換錯誤ClassCastException,陣列索引越界ArrayIndexOutOfBoundsException,使用了空物件NullPointerException等等。
檢查異常(checked exception):除了Error 和 RuntimeException的其它異常。javac強制要求程式設計師為這樣的異常做預備處理工作(使用try…catch…finally或者throws)。在方法中要麼用try-catch語句捕獲它並處理,要麼用throws子句宣告丟擲它,否則編譯不會通過。
這樣的異常一般是由程式的執行環境導致的。因為程式可能被執行在各種未知的環境下,而程式設計師無法干預使用者如何使用他編寫的程式,於是程式設計師就應該為這樣的異常時刻準備著。如SQLException , IOException,ClassNotFoundException 等。
需要明確的是:檢查和非檢查是對於javac來說的,這樣就很好理解和區分了。
錯誤與異常
下面看幾個具體的例子,包括error,exception和throwable
執行時異常,不需要顯示捕獲。檢查異常需要顯示捕獲或者丟擲。
//錯誤即error一般指jvm無法處理的錯誤
//異常是Java定義的用於簡化錯誤處理流程和定位錯誤的一種工具。
public class 錯誤和異常 {
Error error = new Error();
public static void main(String[] args) {
throw new Error();
}
//下面這四個異常或者錯誤有著不同的處理方法
public void error1 (){
//編譯期要求必須處理,因為這個異常是最頂層異常,包括了檢查異常,必須要處理
try {
throw new Throwable();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
//Exception也必須處理。否則報錯,因為檢查異常都繼承自exception,所以預設需要捕捉。
public void error2 (){
try {
throw new Exception();
} catch (Exception e) {
e.printStackTrace();
}
}
//error可以不處理,編譯不報錯,原因是虛擬機器根本無法處理,所以啥都不用做
public void error3 (){
throw new Error();
}
//runtimeexception眾所周知編譯不會報錯
public void error4 (){
throw new RuntimeException();
}
// Exception in thread "main" java.lang.Error
// at com.javase.異常.錯誤.main(錯誤.java:11)
}
異常的處理方式
在編寫程式碼處理異常時,對於檢查異常,有2種不同的處理方式:
使用try…catch…finally語句塊處理它。
或者,在函式簽名中使用throws 宣告交給函式呼叫者caller去解決。
try-catch
(世界上最真情的相依,是你在try我在catch。無論你發神馬脾氣,我都默默承受,靜靜處理。)
public class 異常處理方式 {
@Test
public void main() {
try{
//try塊中放可能發生異常的程式碼。
InputStream inputStream = new FileInputStream("a.txt");
//如果執行完try且不發生異常,則接著去執行finally塊和finally後面的程式碼(如果有的話)。
int i = 1/0;
//如果發生異常,則嘗試去匹配catch塊。
throw new SQLException();
//使用1.8jdk同時捕獲多個異常,runtimeexception也可以捕獲。只是捕獲後虛擬機器也無法處理,所以不建議捕獲。
}catch(SQLException | IOException | ArrayIndexOutOfBoundsException exception){
System.out.println(exception.getMessage());
//每一個catch塊用於捕獲並處理一個特定的異常,或者這異常型別的子類。Java7中可以將多個異常宣告在一個catch中。
//catch後面的括號定義了異常型別和異常引數。如果異常與之匹配且是最先匹配到的,則虛擬機器將使用這個catch塊來處理異常。
//在catch塊中可以使用這個塊的異常引數來獲取異常的相關資訊。異常引數是這個catch塊中的區域性變數,其它塊不能訪問。
//如果當前try塊中發生的異常在後續的所有catch中都沒捕獲到,則先去執行finally,然後到這個函式的外部caller中去匹配異常處理器。
//如果try中沒有發生異常,則所有的catch塊將被忽略。
}catch(Exception exception){
System.out.println(exception.getMessage());
//...
}finally{
//finally塊通常是可選的。
//無論異常是否發生,異常是否匹配被處理,finally都會執行。
//finally主要做一些清理工作,如流的關閉,資料庫連線的關閉等。
}
異常出現時該方法後面的程式碼不會執行,即使異常已經被捕獲。這裡舉出一個奇特的例子,在catch裡再次使用try catch finally。
@Test
public void test() {
try {
throwE();
System.out.println("我前面丟擲異常了");
System.out.println("我不會執行了");
} catch (StringIndexOutOfBoundsException e) {
System.out.println(e.getCause());
}catch (Exception ex) {
//在catch塊中仍然可以使用try catch finally
try {
throw new Exception();
}catch (Exception ee) {
}finally {
System.out.println("我所在的catch塊沒有執行,我也不會執行的");
}
}
}
//在方法宣告中丟擲的異常必須由呼叫方法處理或者繼續往上拋,
// 當拋到jre時由於無法處理終止程式
public void throwE (){
// Socket socket = new Socket("127.0.0.1", 80);
//手動丟擲異常時,不會報錯,但是呼叫該方法的方法需要處理這個異常,否則會出錯。
// java.lang.StringIndexOutOfBoundsException
// at com.javase.異常.異常處理方式.throwE(異常處理方式.java:75)
// at com.javase.異常.異常處理方式.test(異常處理方式.java:62)
throw new StringIndexOutOfBoundsException();
}
注意點:
1、try塊中的區域性變數和catch塊中的區域性變數(包括異常變數),以及finally中的區域性變數,他們之間不可共享使用。
2、每一個catch塊用於處理一個異常。異常匹配是按照catch塊的順序從上往下尋找的,只有第一個匹配的catch會得到執行。匹配時,不僅執行精確匹配,也支援父類匹配,因此,如果同一個try塊下的多個catch異常型別有父子關係,應該將子類異常放在前面,父類異常放在後面,這樣保證每個catch塊都有存在的意義。
3、java中,異常處理的任務就是將執行控制流從異常發生的地方轉移到能夠處理這種異常的地方去。==也就是說:當一個函式的某條語句發生異常時,這條語句的後面的語句不會再執行,它失去了焦點。==執行流跳轉到最近的匹配的異常處理catch程式碼塊去執行,異常被處理完後,執行流會接著在“處理了這個異常的catch程式碼塊”後面接著執行。
有的程式語言當異常被處理後,控制流會恢復到異常丟擲點接著執行,這種策略叫做:resumption model of exception handling(恢復式異常處理模式 )
而Java則是讓執行流恢復到處理了異常的catch塊後接著執行,這種策略叫做:termination model of exception handling(終結式異常處理模式)
“不負責任”的throws
throws是另一種處理異常的方式,它不同於try…catch…finally,throws僅僅是將函式中可能出現的異常向呼叫者宣告,而自己則不具體處理。
採取這種異常處理的原因可能是:方法本身不知道如何處理這樣的異常,或者說讓呼叫者處理更好,呼叫者需要為可能發生的異常負責。
public void foo() throws ExceptionType1 , ExceptionType2 ,ExceptionTypeN
{
//foo內部可以丟擲 ExceptionType1 , ExceptionType2 ,ExceptionTypeN 類的異常,或者他們的子類的異常物件。
}
固執的finally
finally塊不管異常是否發生,只要對應的try執行了,則它一定也執行。只有一種方法讓finally塊不執行:System.exit()。因此finally塊通常用來做資源釋放操作:關閉檔案,關閉資料庫連線等等。
良好的程式設計習慣是:在try塊中開啟資源,在finally塊中清理釋放這些資源。
需要注意的地方:
1、finally塊沒有處理異常的能力。處理異常的只能是catch塊。
2、在同一try…catch…finally塊中 ,如果try中丟擲異常,且有匹配的catch塊,則先執行catch塊,再執行finally塊。如果沒有catch塊匹配,則先執行finally,然後去外面的呼叫者中尋找合適的catch塊。
3、在同一try…catch…finally塊中 ,try發生異常,且匹配的catch塊中處理異常時也丟擲異常,那麼後面的finally也會執行:首先執行finally塊,然後去外圍呼叫者中尋找合適的catch塊。
public class finally使用 {
public static void main(String[] args) {
try {
throw new IllegalAccessException();
}catch (IllegalAccessException e) {
// throw new Throwable();
//此時如果再拋異常,finally無法執行,只能報錯。
//finally無論何時都會執行
//除非我顯示呼叫。此時finally才不會執行
System.exit(0);
}finally {
System.out.println("算你狠");
}
}
}
異常的鏈化
在一些大型的,模組化的軟體開發中,一旦一個地方發生異常,則如骨牌效應一樣,將導致一連串的異常。假設B模組完成自己的邏輯需要呼叫A模組的方法,如果A模組發生異常,則B也將不能完成而發生異常,但是B在丟擲異常時,會將A的異常資訊掩蓋掉,這將使得異常的根源資訊丟失。異常的鏈化可以將多個模組的異常串聯起來,使得異常資訊不會丟失。
異常鏈化 : 以一個異常物件為引數構造新的異常物件。新的異物件將包含先前異常的資訊。這項技術主要是異常類的一個帶Throwable引數的函式來實現的。這個當做引數的異常,我們叫他根源異常(cause)。
檢視Throwable類原始碼,可以發現裡面有一個Throwable欄位cause,就是它儲存了構造時傳遞的根源異常引數。這種設計和連結串列的結點類設計如出一轍,因此形成鏈也是自然的了。
public class Throwable implements Serializable {
private Throwable cause = this;
public Throwable(String message, Throwable cause) {
fillInStackTrace();
detailMessage = message;
this.cause = cause;
}
public Throwable(Throwable cause) {
fillInStackTrace();
detailMessage = (cause==null ? null : cause.toString());
this.cause = cause;
}
}
下面是一個例子,演示了異常的鏈化:從命令列輸入2個int,將他們相加,輸出。輸入的數不是int,則導致getInputNumbers異常,從而導致add函式異常,則可以在add函式中丟擲一個鏈化的異常。
public static void main(String[] args)
{
System.out.println("請輸入2個加數");
int result;
try
{
result = add();
System.out.println("結果:"+result);
} catch (Exception e){
e.printStackTrace();
}
}
//獲取輸入的2個整數返回
private static List<Integer> getInputNumbers()
{
List<Integer> nums = new ArrayList<>();
Scanner scan = new Scanner(System.in);
try {
int num1 = scan.nextInt();
int num2 = scan.nextInt();
nums.add(new Integer(num1));
nums.add(new Integer(num2));
}catch(InputMismatchException immExp){
throw immExp;
}finally {
scan.close();
}
return nums;
}
//執行加法計算
private static int add() throws Exception
{
int result;
try {
List<Integer> nums =getInputNumbers();
result = nums.get(0) + nums.get(1);
}catch(InputMismatchException immExp){
throw new Exception("計算失敗",immExp); /////////////////////////////鏈化:以一個異常物件為引數構造新的異常物件。
}
return result;
}
/*
請輸入2個加數
r 1
java.lang.Exception: 計算失敗
at practise.ExceptionTest.add(ExceptionTest.java:53)
at practise.ExceptionTest.main(ExceptionTest.java:18)
Caused by: java.util.InputMismatchException
at java.util.Scanner.throwFor(Scanner.java:864)
at java.util.Scanner.next(Scanner.java:1485)
at java.util.Scanner.nextInt(Scanner.java:2117)
at java.util.Scanner.nextInt(Scanner.java:2076)
at practise.ExceptionTest.getInputNumbers(ExceptionTest.java:30)
at practise.ExceptionTest.add(ExceptionTest.java:48)
... 1 more
*/
自定義異常
如果要自定義異常類,則擴充套件Exception類即可,因此這樣的自定義異常都屬於檢查異常(checked exception)。
如果要自定義非檢查異常,則擴充套件自RuntimeException。按照國際慣例,自定義的異常應該總是包含如下的建構函式:
一個無參建構函式
一個帶有String引數的建構函式,並傳遞給父類的建構函式。
一個帶有String引數和Throwable引數,並都傳遞給父類建構函式
一個帶有Throwable
引數的建構函式,並傳遞給父類的建構函式。
下面是IOException類的完整原始碼,可以借鑑。
public class IOException extends Exception
{
static final long serialVersionUID = 7818375828146090155L;
public IOException()
{
super();
}
public IOException(String message)
{
super(message);
}
public IOException(String message, Throwable cause)
{
super(message, cause);
}
public IOException(Throwable cause)
{
super(cause);
}
}
異常的注意事項
1、當子類重寫父類的帶有 throws宣告的函式時,其throws宣告的異常必須在父類異常的可控範圍內——用於處理父類的throws方法的異常處理器,必須也適用於子類的這個帶throws方法 。這是為了支援多型。
例如,父類方法throws 的是2個異常,子類就不能throws 3個及以上的異常。父類throws IOException,子類就必須throws IOException或者IOException的子類。
2、Java程式可以是多執行緒的。每一個執行緒都是一個獨立的執行流,獨立的函式呼叫棧。如果程式只有一個執行緒,那麼沒有被任何程式碼處理的異常 會導致程式終止。如果是多執行緒的,那麼沒有被任何程式碼處理的異常僅僅會導致異常所在的執行緒結束。
也就是說,Java中的異常是執行緒獨立的,執行緒的問題應該由執行緒自己來解決,而不要委託到外部,也不會直接影響到其它執行緒的執行。
finally與return的故事
1、首先一個不容易理解的事實:在 try塊中即便有return,break,continue等改變執行流的語句,finally也會執行。
public static void main(String[] args)
{
int re = bar();
System.out.println(re);
}
private static int bar()
{
try{
return 5;
} finally{
System.out.println("finally");
}
}
/*輸出:
finally
*/
2、finally中的return 會覆蓋 try 或者catch中的返回值。
public static void main(String[] args)
{
int result;
result = foo();
System.out.println(result); /////////2
result = bar();
System.out.println(result); /////////2
}
@SuppressWarnings("finally")
public static int foo()
{
try{
int a = 5 / 0;
} catch (Exception e){
return 1;
} finally{
return 2;
}
}
@SuppressWarnings("finally")
public static int bar()
{
try {
return 1;
}finally {
return 2;
}
}
3、finally中的return會抑制(消滅)前面try或者catch塊中的異常。
class TestException
{
public static void main(String[] args)
{
int result;
try{
result = foo();
System.out.println(result); //輸出100
} catch (Exception e){
System.out.println(e.getMessage()); //沒有捕獲到異常
}
try{
result = bar();
System.out.println(result); //輸出100
} catch (Exception e){
System.out.println(e.getMessage()); //沒有捕獲到異常
}
}
//catch中的異常被抑制
@SuppressWarnings("finally")
public static int foo() throws Exception
{
try {
int a = 5/0;
return 1;
}catch(ArithmeticException amExp) {
throw new Exception("我將被忽略,因為下面的finally中使用了return");
}finally {
return 100;
}
}
//try中的異常被抑制
@SuppressWarnings("finally")
public static int bar() throws Exception
{
try {
int a = 5/0;
return 1;
}finally {
return 100;
}
}
}
4、finally中的異常會覆蓋(消滅)前面try或者catch中的異常。
class TestException
{
public static void main(String[] args)
{
int result;
try{
result = foo();
} catch (Exception e){
System.out.println(e.getMessage()); //輸出:我是finaly中的Exception
}
try{
result = bar();
} catch (Exception e){
System.out.println(e.getMessage()); //輸出:我是finaly中的Exception
}
}
//catch中的異常被抑制
@SuppressWarnings("finally")
public static int foo() throws Exception
{
try {
int a = 5/0;
return 1;
}catch(ArithmeticException amExp) {
throw new Exception("我將被忽略,因為下面的finally中丟擲了新的異常");
}finally {
throw new Exception("我是finaly中的Exception");
}
}
//try中的異常被抑制
@SuppressWarnings("finally")
public static int bar() throws Exception
{
try {
int a = 5<