Java 之路 (十二) -- 異常(基本常識、try-catch-finally、throw、throws、使用指南)

  • 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 又分為兩類:

  1. 不受檢查的異常:RuntimeException

    1. 這種異常屬于錯誤,即便處理也無法使程序恢復,導致的原因通常是因為錯誤操作(比如使用 null 引用)或者程序員的疏忽(比如數組越界),這部分是由 Java 運行時檢測處理,編譯器不會檢查,因此代碼中對該類異常需要忽略。

      自動被 Java 虛擬機拋出,自動進行捕獲。

  2. 被檢查的異常:除了 RuntimeException 及其子類 以外的 Exception 子類

    1. 程序員處理的實際上就是這部分異常,編譯時會被強制檢查。

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. 異常使用指南

  1. 在恰當的級別處理問題(在知道該如何處理的情況下才捕獲異常)。
  2. 解決問題并且重新調用產生異常的方法。
  3. 進行少許修補,然后繞過異常發生的地方繼續執行。
  4. 用別的數據進行計算,以代替方法預計會返回的值。
  5. 把當前運行環境下能做的事情盡量做完,然后把相同的異常重拋到更高層。
  6. 把當前運行環境下能做的事情盡量做完,然后把不同的異常拋到更高層。
  7. 終止程序。
  8. 進行簡化(如果你的異常模式使問題變得太復雜,那用起來會非常痛苦也很煩人)。
  9. 讓類庫和程序更安全(這既是在為調試做短期投資,也是在為程序的健壯性做長期投資)。
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容