愉快地學Java語言:第十一章 異常處理

導讀

本文適合Java入門,不太適合Java中高階軟體工程師。本文以《Java程式設計基礎篇》第10版為藍本,採用不斷提出問題,然後解答問題的方式來講述。本篇文章只是這個系列中的一篇,如果你喜歡這種講解方式,或者覺得從中能學到知識,可以關注我,以便查閱本系列其他文章。

讓我們開始愉快地學習Java語言吧!

愉快地學Java語言:第十一章 異常處理

1基本概念

執行時錯誤:程式執行過程中,JVM檢測到一個不可能完成的操作而丟擲錯誤。

異常:它是異常類的例項,表示阻止程式正常執行的錯誤或情況。

異常處理器:處理異常的程式碼。

捕獲一個異常:尋找處理器的過程。

異常在什麼地方丟擲呢?

異常可以在方法中丟擲。

異常應該被誰捕獲呢?

異常被方法的呼叫者捕獲。

2異常型別有哪些?

看下面這個UML,

愉快地學Java語言:第十一章 異常處理

所有型別的異常都擴充套件自Throwable,它有兩個子類Error和Exception。Error和Exception也有許多子類,例如:

愉快地學Java語言:第十一章 異常處理

Error也有一些子類,例如:

愉快地學Java語言:第十一章 異常處理

異常型別分為必檢異常(checked exception)和免檢異常(unchecked exception)

必檢異常指編譯器強制程式設計師檢測這種異常,並使用try-catch處理或者在方法頭宣告必檢異常。

免檢異常不強制程式設計師檢測和處理這種異常。免檢異常包括:RuntimeException、 Error及其子類。

除RuntimeException、 Error及其子類外都是必檢異常。

3如何宣告異常?

在方法頭使用throws關鍵字宣告異常,多個異常用逗號分隔。

例如,類A的方法f宣告丟擲IOException

愉快地學Java語言:第十一章 異常處理

4如何丟擲異常呢?

使用throw語句丟擲異常。

語法為 :

throw 異常例項;

5如何捕獲異常呢?

將方法用try-catch塊包裹起來就可以在catch塊中捕獲異常。

語法為:

try{

//方法

}catch{

//處理異常

}

或者

try{

//方法

}catch(異常型別 ex){

//處理異常

}

或者,一次捕獲多個異常,這種方式要注意子類應該排在父類前面。

try {

//方法

}

catch (Exceptionl exVarl){

//處理異常

}catch (Exception2 exVar2){

//處理異常

}

。。

catch (ExceptionN exVarN){

//處理異常

}

如果try塊中不止一條語句,丟擲異常的語句後面還有一條或多條,那麼後面的語句會執行嗎?

不會,將跳過try塊後面的語句,直接跳到catch塊。

如果在執行try塊的時候沒有異常丟擲,那麼catch塊會執行嗎?

當然不會,將跳過catch塊執行下面的語句。

捕獲多個異常時,是如何匹配異常引數的呢?

依次查詢catch塊的引數,直到找到相匹配的異常型別後,就進入那個catch塊。這裡“相匹配的異常”有兩層含義,一是try塊中語句丟擲的異常和catch塊的引數型別相同,或者try塊中語句丟擲的異常是catch塊的引數型別的子型別。

如果多個方法形成了呼叫鏈,如何找到異常處理器呢?

從當前的方法開始,沿著方法呼叫鏈,按照異常的反向傳播方向找到異常處理器。

我們驗證一下:

在類A中定義了f1,f2,f3這三個方法,f1呼叫了f2,f2呼叫了f3,只有f3會丟擲異常並處理它。

愉快地學Java語言:第十一章 異常處理

在main方法中呼叫f1,觀察發現f3處理了異常,並將異常的追蹤資訊打印出來。

愉快地學Java語言:第十一章 異常處理

若在f2中新增對異常的處理,而f3不處理異常,然後觀察結果,先將A按如下修改:

愉快地學Java語言:第十一章 異常處理

呼叫f1,觀察執行結果,透過列印的資訊,依然可以追蹤到哪一行真正丟擲異常:

愉快地學Java語言:第十一章 異常處理

此外,一次捕獲多個異常還有一種簡化的寫法:

catch (Exceptionl|Exception2|… |Exceptionk ex){

//處理異常

}

依然要注意catch的引數子類在前,父類在後。

6從異常中獲得資訊

獲取異常資訊常用的幾個方法有:

printStackTrace():顯示棧跟蹤

getStackTrace():獲得棧跟蹤

getMessage():異常資訊

toString():異常物件和資訊

讓我們把上例中A類的方法修改一下,列印上述方法返回的資訊,讓我們看看具體的資訊。

愉快地學Java語言:第十一章 異常處理

愉快地學Java語言:第十一章 異常處理

7finally子句

當異常從一個方法丟擲時,如果還有一些收尾工作沒有完成,那麼也會被異常中斷,例如,操作一個網路資源之前需要建立一個連線到網路資源的連線物件,由於每個連線都會消耗掉一部分資源,所有操作完成,獲得結果後,要關閉連線,釋放連線所佔用的資源,如果呼叫操作網路資源的方法丟擲異常,那麼後續的操作將無法進行,導致無法釋放連線所佔用的資源。

如何做到即使丟擲異常也可以不中斷釋放資源呢?

使用finally子句可以解決問題。

語法結構為:

try {

//可能會丟擲異常的語句

}catch(異常型別 ex){

//處理異常

} finally {

//收尾工作

}

不論何種情況,finally子句都會執行,即使在finally子句之前有return語句。但要特別留意的是,finally子句中的程式碼也有可能丟擲異常。

8何時使用異常呢?

不能亂用異常,也許對不可能丟擲異常的語句使用try-catch也能使程式正常執行,但是那樣會使程式碼糟糕難懂。

對可能丟擲異常的地方,並且要對這種異常進行處理的時候,我們才使用try-catch。

同時,注意不要把異常處理算作業務邏輯的一部分。

9重新丟擲異常

當對捕獲的異常進行處理後,還想將異常向上傳遞,那麼可以在catch塊中丟擲異常,可以對已捕獲的異常進行包裝後,在丟擲。

10 鏈式異常

和其他異常一起丟擲一個異常,就構成了鏈式異常。

鏈式異常有啥好處呢?

愉快地學Java語言:第十一章 異常處理

我們看到,新定義的異常資訊連同f3中的異常資訊一起輸出。並且指明引起異常的原因。

愉快地學Java語言:第十一章 異常處理

11自定義異常類

如果JDK API提供的異常愉快地學Java語言:第十章:抽象類和介面型別不能滿足需求,那麼可以派生java。lang。Exception類來定義一個異常。

愉快地學Java語言:第十一章 異常處理

當我們擴充套件Exception時,編譯器會提示我們新增一個序列化ID,那麼為啥需要這個呢?

簡單講就是,如果使用者沒有自己宣告一個serialVersionUID,介面會預設生成一個serialVersionUID,因為預設的serialVersinUID對於class的細節非常敏感,反序列化時可能會導致InvalidClassException這個異常。所以還是新增的好。

另外,注意一般不要擴充套件免檢異常,通常我們自定義異常就是為了檢測它並捕獲它。

或許你還想看看往期文章:

愉快地學Java語言:第十章:抽象類和介面

愉快地學Java語言:第九章 面向物件基本特徵 第1講

愉快地學Java語言:第九章 面向物件基本特徵 第2講

愉快地學Java語言:第九章 面向物件基本特徵 第3講