- Java 的基本理念是 “結構不佳的代碼不能運行”
- 錯誤回復在我們編寫的每個程序中都是基本的元素
- Java 的主要目標之一是創建供他人使用的程序構件
- Java 的異常處理的目的在于通過使用少于目前數量的代碼來簡化大型、可靠的程序的生成,并且通過這種方式可以使你更加自信:你的應用中沒有未處理的錯誤
1. 初識異常
正式介紹異常之前,先來看一個例子:
public static void readFileByBytes(String fileName) {
// 一般先創建file對象
FileInputStream fileInput = null;
try {
File file = new File(fileName);
if (!file.exists()) {
file.createNewFile();
}
byte[] buffer = new byte[1024];
fileInput = new FileInputStream(file);
int byteread = 0;
// byteread表示一次讀取到buffers中的數量。
while ((byteread = fileInput.read(buffer)) != -1) {
System.out.write(buffer, 0, byteread);
}
} catch (Exception e) {
// TODO: handle exception
} finally {
try {
if (fileInput != null) {
fileInput.close();
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
上述代碼展示了一種進行文件讀取的方式,是一個運用 Java 異常處理的典型例子。
可能你會有疑問:try、catch、finally 是什么?為什么要寫成這個格式?如果不這么寫會發生什么情況?
讓我們假設這樣一個情況:假如在我調用語句 File file = new File(fileName);
創建 File時,由于某種原因未創建成功(比如內存不足,hhh),那么當我后面使用這個不存在的 file 時,就會發生錯誤,我們的程序會無法繼續運行。這種情況下,我們就可以說該程序拋出了一個“異常”,try 就可以理解成“我需要嘗試執行一下正常的流程,但是其中會出現某些錯誤”,之后 Java 會通過 catch 來對這個異常進行"捕獲",描述發生某種錯誤時應該做些什么。
而當無論是否發生了錯誤,我都需要來做一些清理工作時,就需要使用 finally 語句。finally 中的語句無論如何最終都會執行,這樣即便我的代碼在中間某部分出錯了,但是清理工作依舊可以執行。
上述知識一個基本的介紹,下面我們就一步步學習 Java 中的異常處理機制。
2. 異常的基本常識
2.1 異常和錯誤
首先需要理解一下關于異常和錯誤的關系:
- 異常本身只是一個對象,其中包含了錯誤信息
- 實際上,異常只是我們用來處理錯誤的手段。我們的代碼中可能會出現各種各樣的錯誤,比如使用了一個空引用,比如除數為 0,這些錯誤會使我們的程序無法繼續運行。如果某種情況下,我并不知道如何處理這種錯誤,那么在這個時候,就需要創建一個代表了錯誤信息的對象,并將其從當前環境中"拋出"( 拋出異常 ),把錯誤信息傳播到其他環境,表示"這里出現了一個問題,但我無法處理,我把它交給你處理",然后尋找一個恰當的地方來處理這個錯誤,繼續執行程序(否則程序就終止了)。
2.2 異常的拋出
上面我們提到了 拋出異常,下面具體看看拋出異常時會發生哪些事。
- 當程序遇到某些阻止當前方法或作用域繼續執行的問題時,且當前環境下無法獲得必要的信息來解決問題,于是從當前環境跳出,這問題提交給上一級環境,即拋出異常
- 拋出異常會發生的事:
- 首先使用 new 在堆上創建異常對象
- 然后,當前執行路徑被終止,并且從當前環境中彈出對異常對象的引用
- 此時,異常處理機制接管程序,并在異常處理程序中繼續執行程序,其任務就是將程序從錯誤狀態中恢復,以使程序要么換一種方式運行,要么繼續運行下去。
2.3 Java 標準異常
Java 標準庫中內建了一系列的異常,頂級父類為 Throwable,表示任何可以作為異常被拋出的類。
Throwable 可以被分為兩種類型:
- Error :用來表示編譯時和系統錯誤,是Java 運行環境的內部錯誤或者硬件問題,如內存不足等,程序員通常無須關心如何處理(準確來講是無能為力,除了退出別無他法)。
- Exception:可以被拋出的基本類型,在 Java 類庫、用戶方法以及運行時故障中都可能拋出 Exception 型異常,是異常處理的核心,我們需要關心的基類型就是 Exception。
圖摘自 http://www.cnblogs.com/lulipro/p/7504267.html
聲明一點:上面的結構是不完整的,但是在下太懶了,直接接用上述文章的圖,感興趣可以自行查看官方文檔進行整理。
實際上,對于 Java 編譯時是否對異常進行檢查,Exception 又分為兩類:
-
不受檢查的異常:RuntimeException
-
這種異常屬于錯誤,即便處理也無法使程序恢復,導致的原因通常是因為錯誤操作(比如使用 null 引用)或者程序員的疏忽(比如數組越界),這部分是由 Java 運行時檢測處理,編譯器不會檢查,因此代碼中對該類異常需要忽略。
自動被 Java 虛擬機拋出,自動進行捕獲。
-
-
被檢查的異常:除了 RuntimeException 及其子類 以外的 Exception 子類
- 程序員處理的實際上就是這部分異常,編譯時會被強制檢查。
2.4 自定義異常
通常異常的名稱代表發生的問題,并且異常的名稱應該可以望文知義
如果需要自定義異常,必須繼承已有的異常類,最好是選擇意思相近的異常類繼承,使名字做到望名生義。
按照國際慣例,自定義的異常應該總是包含如下的構造函數:
- 一個無參構造函數
- 一個帶有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); } }
2.5 異常的意義
- 異常最重要的方面之一就是如果發生問題,他們將不允許程序沿著其正常的路徑繼續走下去。一場允許我們強制程序停止運行,并告訴我們出現了什么問題,或者強制程序處理問題,并返回到穩定狀態
- 異常代表了當前方法不能繼續執行的情形。開發異常處理系統的原因是,如果為每個方法所有可能發生的錯誤都進行處理的話,任務舊顯得過于繁重了,結果常常是將錯誤忽略。開發的初衷是為了方便程序員處理錯誤。
- 異常處理的重要原則是“只有在知道如何處理的情況下才捕獲異常”,實際上,異常處理的一個重要目標就是把錯誤處理的代碼同錯誤發生的地點相分離。
3. 異常處理機制
對于異常處理,有兩種方式
- try - catch - [finally] 處理
- 函數聲明中使用 throws 進行異常說明
如下:
void f() throws 潛在異常列表{//throws 表示可能有一些異常我無法處理,于是向上級拋出
//try-catch-finally 處理當前信息足以解決的異常
try{
//可能拋出異常的方法調用
}catch(SomeException se) {
//必備
//異常處理
}finally{
//可選
//一些清理工作
}
}
一些基礎:
- 調用棧:展示了到異常拋出地點的方法調用序列。
3.1 捕獲異常
try - catch
try:該塊內執行可能產生異常的方法調用
-
catch:即異常處理程序,針對每個要捕獲的異常,準備相應的處理程序
catch 必須緊跟 try 之后,異常拋出時,異常處理機制將負責搜尋參數與異常類型相匹配的第一個處理程序。然后進入 catch 子句執行,此時認為異常得到了處理。
一旦 catch 子句結束,則處理程序的查找過程結束。
注意:只有匹配的第一個 catch 子句能執行。
try{ //可能產生異常的代碼 }catch(ExceptionType1 id1){ //處理該異常 }catch(ExceptionType1 id2){ //處理該異常 }...
終止模型 & 恢復模型
- Java 支持終止模型:錯誤非常關鍵,以至于程序無法回到一場發生的地方繼續執行,一旦一場被拋出,就表明錯誤已無法挽回,也不能回來繼續執行
- 恢復模型:異常處理程序的工作室修正錯誤,然后重新嘗試調用出問題的方法,并認為第二次能成功。通常希望亦常被處理之后能繼續執行程序。
- Java 可以實現恢復模型:將 try 塊放在 while 循環利,這樣不斷地進入 try 塊,知道滿意為止;或者遇見錯誤時不拋出異常,而是調用方法修整
- 為什么 Java 不采用恢復模型:關鍵在于耦合,恢復性的處理程序需要了解異常拋出地地點,這勢必要包含依賴于拋出位置地非通用代碼。
重新拋出異常:在 catch 中捕獲異常后,得到了對當前異常對象的引用,此時可以直接把它重新拋出。
-
如果只是把當前異常對象重新拋出,那么 printStackTrace() 方法顯式地將是原異常拋出點地調用棧信息。
通過調用 fillInStackTrace() 方法可以更新該信息。該方法返回一個 Throwable 對象,它是通過把當前調用棧信息填入原來那個異常對象而建立的。
3.1.2 異常匹配
拋出異常時,會按照代碼的書寫順序找最近的處理程序,找到匹配的處理程序之后,就會認為異常將得到處理,然后不再繼續查找。
-
查找時,派生類的對象也可以匹配基類的處理程序
因此通常將子類異常放在前面,父類異常放在后面,保證每個 catch 塊都有意義
3.1.3 finally 進行清理
用于把除了內存之外的資源恢復到其初始狀態。
finally 子句總能執行
- 無論是否捕獲異常,finally 總能被執行
- 遇到 return 時,會在return之前執行 finally 里的語句,然后再進行 return
3.2 拋出異常
3.2.1 異常拋出
throw
通過 new 創建異常對象后,引用傳遞給 throw。
-
throw & return
相似:
- throw 從效果上看就像是從方法“返回”的
- 能用拋出異常的方式從當前的作用域中退出
不同
- 異常返回的地點與普通調用放回的地點完全不同
3.2.2 異常鏈
異常鏈:在捕獲一個異常后拋出另一個異常,并且把原始異常的的信息保存下來。
- Throwable 的子類在構造器中接收一個 cause 對象作為參數,這個 cause 就用來表示原始異常,這樣把原始異常傳遞給新的異常,使得及時在當前位置創建并拋出了新的異常。也能通過這個異常鏈追蹤到異常最初發生的位置。
- 所有Throwable 的子類只有三種基本的異常類提供了帶cause參數的構造器:Error、Exception、以及RuntimrException
- 如果要把其他類型的異常連接起來,那么需要使用initCause方法
3.2.3 異常聲明
如果一個方法內部的代碼會拋出檢查異常(checked exception),而方法自己又沒有完全處理掉,則 javac 保證你必須在方法的簽名上使用 throws 關鍵字聲明這些可能拋出的異常,否則編譯不通過。
它屬于方法聲明的一部分,緊跟在形參列表之后,使用關鍵字 throws + 潛在異常類型的列表,僅僅是將函數中可能出現的異常向調用者聲明,而自己則不具體處理。
void f() throws 潛在異常列表{
//...
}
3.3 注意事項
覆蓋方法的時候,只能拋出在基類方法的異常說明里列出的那些異常。
- 對構造器不起作用,可以拋出任何異常。
派生類構造器的異常說明必須包括基類構造器的異常說明。
派生類構造器不能捕獲基類構造器拋出的異常。
對于在構造階段可能會拋出異常,并且要求清理的類,最安全的使用方式時使用嵌套的 try 語句。
- 基本規則是:在創建需要清理的對象之后,立即進入一個 try-finally 語句
異常處理機制的好處:
- 往往能夠降低錯誤處理代碼的復雜度
- 用強制規定的形式來消除錯誤處理過程中隨心所欲的因素
4. 異常使用指南
- 在恰當的級別處理問題(在知道該如何處理的情況下才捕獲異常)。
- 解決問題并且重新調用產生異常的方法。
- 進行少許修補,然后繞過異常發生的地方繼續執行。
- 用別的數據進行計算,以代替方法預計會返回的值。
- 把當前運行環境下能做的事情盡量做完,然后把相同的異常重拋到更高層。
- 把當前運行環境下能做的事情盡量做完,然后把不同的異常拋到更高層。
- 終止程序。
- 進行簡化(如果你的異常模式使問題變得太復雜,那用起來會非常痛苦也很煩人)。
- 讓類庫和程序更安全(這既是在為調試做短期投資,也是在為程序的健壯性做長期投資)。