Java異常詳解

hi,你們好,我是開發者FTD。今天咱們來聊一聊Java語言中的異常處理機制。前端

Java 語言誕生於1995年,距如今已經有26年的時間了。做爲一門比較老的語言依然擁有強大的生命力,Java在不少方面(例如高併發,移植性等)具備明顯的優點,固然在一些方面(例如圖像處理)也有不足,今天要給你們介紹的異常就是Java語言中提供的一個強大的,可讓咱們正確合理的應對程序中發生錯誤的機制。java

1、異常介紹

什麼是異常?

異常是指程序在運行過程當中發生的,因爲外部問題致使的程序運行異常事件,異常的發生每每會中斷程序的運行。在 Java 這種面向對象的編程語言中,萬物都是對象,異常自己也是一個對象,程序發生異常就會產生一個異常對象。git

異常的分類

講到異常的分類,就不能不說一下Java異常的繼承結構。以下圖所示:程序員

從圖中能夠看到,異常主要有如下類構成:github

  • Throwable
  • Error
  • Exception

接下來咱們就分別介紹一下這幾個基類的做用。面試

Throwable

Throwable 類是 Java 語言中全部錯誤或異常的頂層父類,其餘異常類都繼承於該類。Throwable類有兩個重要的子類:Exception(異常)Error(錯誤),兩者都是 Java 異常處理的重要子類,各自都包含大量子類。數據庫

只有當對象是此類或其子類的實例時,才能經過 Java 虛擬機或者 Java throw 語句拋出。相似地,只有此類或其子類才能夠是 catch 子句中的參數類型。編程

Throwable 對象中包含了其線程建立時線程執行堆棧的快照,它還包含了給出有關錯誤更多信息的消息字符串。segmentfault

最後,它還能夠包含 cause(緣由):另外一個致使此 throwable 拋出的 throwable。此 cause 設施在 1.4 版本中首次出現。它也稱爲異常鏈設施,由於 cause 自身也會有 cause,依此類推,就造成了異常鏈,每一個異常都是由另外一個異常引發的。後端

Error

Error 是 Throwable 的子類,一般狀況下應用程序不該該試圖捕獲的嚴重問題

Error 是程序沒法處理的錯誤,表示運行應用程序中較嚴重問題。大多數錯誤與代碼編寫者執行的操做無關,而表示代碼運行時 JVM(Java 虛擬機)出現的問題。

例如:Java虛擬機運行錯誤(Virtual MachineError),當 JVM 再也不有繼續執行操做所需的內存資源時,將出現 OutOfMemoryError。這些異常發生時,Java虛擬機(JVM)通常會選擇線程終止。

這些錯誤表示故障發生於虛擬機自身、或者發生在虛擬機試圖執行應用時,如Java虛擬機運行錯誤(Virtual MachineError)、類定義錯誤(NoClassDefFoundError)等。這些錯誤是不可查的,由於它們在應用程序的控制和處理能力之 外,並且絕大多數是程序運行時不容許出現的情況。對於設計合理的應用程序來講,即便確實發生了錯誤,本質上也不該該試圖去處理它所引發的異常情況。在 Java中,錯誤經過Error的子類描述。

Exception

Exception以及它的子類,表明程序運行時發送的各類不指望發生的事件。能夠被Java異常處理機制使用,是異常處理的核心。

Exception 異常主要分爲兩類:

一、非檢查性異常(unchecked exception)

Error 和 RuntimeException 以及他們的子類。Java語言在編譯時,不會提示和發現這樣的異常,不要求在程序中處理這些異常。因此咱們能夠在程序中編寫代碼來處理(使用try…catch…finally)這樣的異常,也能夠不作任何處理。對於這些錯誤或異常,咱們應該修正代碼,而不是去經過異常處理器處理。這樣的異常發生的緣由多半是因爲咱們的代碼邏輯出現了問題。

例如:

  • 當程序中用數字除以0時,就會拋出ArithmeticException異常;
  • 在類型轉換時,錯誤的強制類型轉換會拋出ClassCastException類型轉換異常;
  • 當使用集合進行數組索引越界時就會拋出ArrayIndexOutOfBoundsException異常;
  • 當程序中使用了空對象進行操做時就會拋出註明的空指針NullPointerException異常等。

常見的非檢查性異常有

異常 描述
ArithmeticException 當出現異常的運算條件時,拋出異常。例如,一個整數「除以零」時,拋出此類的一個實例。
ArrayIndexOutOfBoundsException 用非法索引訪問數組時跑出的異常。若是索引爲負或大於等於數組大小,則該索引爲非法索引。
ArrayStoreException 試圖將錯誤類型的對象存儲到一個對象數組時,拋出的異常。
ClassCastException 試圖將對象強制轉換爲不是同一個類型或其子類的實例時,拋出的異常。
IllegalArgumentException 當向一個方法傳遞非法或不正確的參數時,拋出該異常。
IllegalMonitorStateException 當某一線程已經試圖等待對象的監視器,或者通知其餘正在等待該對象監視器的線程,而該線程自己沒有得到指定監視器時拋出該異常。
IllegalStateException 在非法或不適當的時間調用方法時產生的信號。或者說Java環境或應用程序沒有處於請求操做所要求的適當狀態下。
IllegalThreadStateException 線程沒有處於請求操做所要求的適當狀態時,拋出該異常。
IndexOutOfBoundsException 當某種排序的索引超出範圍時拋出的異常,例如,一個數組,字符串或一個向量的排序等。
NegativeArraySizeException 若是應用程序試圖建立大小爲負的數組時,拋出該異常。
NullPointerException 當應用程序在須要操做對象的時候而得到的對象實例是null時拋出該異常。
NumberFormatException 當應用程序試圖將字符串轉換成一種數值類型,但該字符串不能轉換爲適當格式時,拋出該異常。
SecurityException 由安全管理器拋出的異常,指示存在安全侵犯。
StringIndexOutOfBoundsException 此異常由String方法拋出,說明索引爲負或者超出了字符串的大小。

二、檢查性異常(checked exception)

除了Error 和 RuntimeException的其它異常。Java語言強制要求程序員爲這樣的異常作預備處理工做(使用try…catch…finally或者throws)。在方法中要麼用try-catch語句捕獲它並處理,要麼用throws子句聲明拋出它,不然編譯不會經過。這樣的異常通常是由程序的運行環境致使的。由於程序可能被運行在各類未知的環境下,而程序員沒法干預用戶如何使用他編寫的程序,因而程序員就應該爲這樣的異常時刻準備着。如SQLException,IOException,ClassNotFoundException 等。

檢查性異常就是指,編譯器在編譯期間要求必須獲得處理的那些異常,你必須在編譯期處理了。

常見的檢查性異常有

異常 描述
ClassNotFoundException 當應用程序試圖加載一個類,經過名字查找時卻發現沒有該類的定義時,拋出該異常。
CloneNotSupportedException 當去克隆一個對象時,發現該對象沒有實現Cloneable接口時,拋出該異常。
IllegalAccessException 當應用程序嘗試經過反射的方式來訪問類、成員變量或調用方法時,卻沒法訪問這些類、成員變量或方法的定義時,拋出該異常。
InstantiationException 當試圖使用Class類中的newInstance方法建立一個類的實例,而制定的類對象由於是一個接口或是一個抽象類而沒法實例化時,拋出該異常。
InterruptedException 一個線程被另外一個線程中斷時,拋出該異常。
NoSuchFieldException 當找不到指定的變量字段時,拋出該異常、
NoSuchMethodException 當找不到指定的類方法時,拋出該異常。

2、初識異常

下面咱們經過一個簡單實例,讓你們更直觀的認識一下Java的異常。

下面的代碼會拋出著名的空指針異常:NullPointerException。

public class Test {
    private int a = 1;
    private int b = 2;

    public static void main(String[] args) {
        Test t1 = new Test();
        Test t2 = null;
        System.out.println(t1.a);
        System.out.println(t2.a);
        System.out.println(t2.c());
    }

    public String c() {
        return "微信公衆號:我是開發者FTD";
    }
}

運行程序,控制檯輸出結果以下:

1
Exception in thread "main" java.lang.NullPointerException
    at cc.devclub.ftd.Test.main(Test.java:11)

Process finished with exit code 1

從控制檯輸出能夠看到,程序打印了 「1」,而後在程序的第11行的位置拋出了 java.lang.NullPointerException ,而後程序就終止運行了。

3、異常處理機制

在編寫代碼處理異常時,對於檢查性異常,有兩種不一樣的處理方式:

  • 使用 try…catch…finally… 語句塊處理
  • 在方法中使用 throws/throw 關鍵詞將異常交給方法調用者去處理
try...catch...finally… 關鍵字
  • 使用 try 和 catch 關鍵字能夠捕獲異常。
  • try/catch 代碼塊放在異常可能發生的地方。

try/catch代碼塊中的代碼稱爲保護代碼,使用 try/catch 的語法以下:

try {
    ...
} catch (IOException ioException) {
    ...
} catch (Exception exception) {
    ...
} finally {
    ...
}

try 塊:

  • try塊中放可能發生異常的代碼。
  • 若是執行完try且不發生異常,則接着去執行finally塊中的代碼和finally後面的代碼(若是有的話)。
  • 若是程序發生異常,則嘗試去匹配對應的catch塊。

catch 塊:

  • 每個catch塊用於捕獲並處理一個特定的異常,或者這異常類型的子類。Java7中能夠將多個異常聲明在一個catch中。
  • catch後面的括號定義了異常類型和異常參數。若是異常與之匹配且是最早匹配到的,則虛擬機將使用這個catch塊來處理異常。
  • 在catch塊中可使用這個塊的異常參數來獲取異常的相關信息。異常參數是這個catch塊中的局部變量,其它塊不能訪問。
  • 若是當前try塊中發生的異常在後續的全部catch中都沒捕獲到,則先去執行finally,而後到這個方法的外部調用者中去匹配異常處理器。
  • 若是try中沒有發生異常,則全部的catch塊將被忽略。

須要注意的地方

一、try塊中的局部變量和catch塊中的局部變量(包括異常變量),以及finally中的局部變量,他們之間不可共享使用。

二、每個catch塊用於處理一個異常。異常匹配是按照catch塊的順序從上往下尋找的,只有第一個匹配的catch會獲得執行。匹配時,不只運行精確匹配,也支持父類匹配,所以,若是同一個try塊下的多個catch異常類型有父子關係,應該將子類異常放在前面,父類異常放在後面,這樣保證每一個catch塊都有存在的意義。

三、Java中,異常處理的任務就是將執行控制流從異常發生的地方轉移到可以處理這種異常的地方去。也就是說:當一個方法的某條語句發生異常時,這條語句的後面的語句不會再執行,它失去了焦點。執行流跳轉到最近的匹配的異常處理catch代碼塊去執行,異常被處理完後,執行流會接着在「處理了這個異常的catch代碼塊」後面接着執行。

finally 塊:

  • finally塊不是必須的,一般是可選的。
  • 不管異常是否發生,異常是否匹配被處理,finally中的代碼都會執行。
  • 一個try至少要有一個catch塊,不然, 至少要有1個finally塊。可是finally不是用來處理異常的,finally不會捕獲和處理異常,處理異常的只能是catch塊。
  • finally主要作一些清理工做,如流的關閉,數據庫鏈接的關閉等。
  • finally塊無論異常是否發生,只要對應的try執行了,則它必定也執行。只有一種方法讓finally塊不執行:System.exit()

你們須要養成良好的編程習慣是:在try塊中打開資源,在finally塊中清理並釋放這些資源,以避免形成內存泄露。

須要注意的地方:

一、在同一try…catch…finally…塊中,若是try中拋出異常,且有匹配的catch塊,則先執行catch塊,再執行finally塊。若是沒有catch塊匹配,則先執行finally,而後去到上層的調用者中尋找合適的catch塊。

二、在同一try…catch…finally…塊中 ,try發生異常,且匹配的catch塊中處理異常時也拋出異常,那麼後面的finally也會執行:首先執行finally塊,而後去上層調用者中尋找合適的catch塊。

throws/throw 關鍵字
  • throws 關鍵字

若是一個方法內部的代碼會拋出檢查性異常(checked exception),而方法本身又沒有對這些異常徹底處理掉,則java的編譯器會要求你必須在方法的簽名上使用 throws 關鍵字聲明這些可能拋出的異常,不然編譯不經過。

throws 是另外一種處理異常的方式,它不一樣於try…catch…finally…,throws 關鍵字僅僅是將方法中可能出現的異常向調用者拋出,而本身則不具體處理。

採起這種異常處理的緣由多是:方法自己不知道如何處理這樣的異常,或者說讓調用者處理更好,調用者須要爲可能發生的異常負責。

  • throw 關鍵字

咱們也能夠經過 throw 語句手動顯式的拋出一個異常,throw語句的後面必須是一個異常對象。語法以下:

throw exceptionObject

throw 語句必須寫在方法中,執行throw 語句的地方就是一個異常拋出點,它和由JRE自動造成的異常拋出點沒有任何差異。

public void save(User user) {
    if (user == null)
        throw new IllegalArgumentException("User對象爲空");
    //......
}
try-catch-finally 的執行順序

try-catch-finally 執行順序的相關問題能夠說是各類面試中的「常客」了,尤爲是 finally 塊中帶有 return 語句的狀況。咱們直接看幾道面試題:

面試題一:

public static void main(String[] args) {
    int result = test1();
    System.out.println(result);
}

public static int test1() {
    int i = 1;
    try {
        i++;
        System.out.println("try block, i = " + i);
    } catch (Exception e) {
        i--;
        System.out.println("catch block i = " + i);
    } finally {
        i = 10;
        System.out.println("finally block i = " + i);
    }
    return i;
}

你們不妨算一算程序員最終運行的結果是什麼。

輸出結果以下:

try block, i = 2
finally block i = 10
10

這算一個至關簡單的問題了,沒有坑,下面咱們稍微改動一下:

public static int test2() {
    int i = 1;
    try {
        i++;
        throw new Exception();
    } catch (Exception e) {
        i--;
        System.out.println("catch block i = " + i);
    } finally {
        i = 10;
        System.out.println("finally block i = " + i);
    }
    return i;
}

輸出結果以下:

catch block i = 1
finally block i = 10
10

運行結果想必也是意料之中吧,程序拋出一個異常,而後被本方法的 catch 塊捕獲並進行了處理。

面試題二:

public static void main(String[] args) {
    int result = test3();
    System.out.println(result);
}

public static int test3() {
    //try 語句塊中有 return 語句時的總體執行順序
    int i = 1;
    try {
        i++;
        System.out.println("try block, i = " + i);
        return i;
    } catch (Exception e) {
        i++;
        System.out.println("catch block i = " + i);
        return i;
    } finally {
        i = 10;
        System.out.println("finally block i = " + i);
    }
}

輸出結果以下:

try block, i = 2
finally block i = 10
2

是否是有點疑惑?明明我 try 語句塊中有 return 語句,可爲何最終仍是執行了 finally 塊中的代碼?

咱們反編譯這個類,看看這個 test3 方法編譯後的字節碼的實現:

0: iconst_1         //將 1 加載進操做數棧
1: istore_0         //將操做數棧 0 位置的元素存進局部變量表
2: iinc          0, 1   //將局部變量表 0 位置的元素直接加一(i=2)
5: getstatic     #3     // 5-27 行執行的 println 方法                
8: new           #5                  
11: dup
12: invokespecial #6                                                     
15: ldc           #7 
17: invokevirtual #8                                                     
20: iload_0         
21: invokevirtual #9                                                     
24: invokevirtual #10                
27: invokevirtual #11                 
30: iload_0         //將局部變量表 0 位置的元素加載進操做棧(2)
31: istore_1        //把操做棧頂的元素存入局部變量表位置 1 處
32: bipush        10 //加載一個常量到操做棧(10)
34: istore_0        //將 10 存入局部變量表 0 處
35: getstatic     #3  //35-57 行執行 finally中的println方法             
38: new           #5                  
41: dup
42: invokespecial #6                  
45: ldc           #12                 
47: invokevirtual #8                  
50: iload_0
51: invokevirtual #9                
54: invokevirtual #10                 
57: invokevirtual #11                 
60: iload_1         //將局部變量表 1 位置的元素加載進操做棧(2)
61: ireturn         //將操做棧頂元素返回(2)
-------------------try + finally 結束 ------------
------------------下面是 catch + finally,相似的 ------------
62: astore_1
63: iinc          0, 1
.......
.......

從咱們的分析中能夠看出來,finally 代碼塊中的內容始終會被執行,不管程序是否出現異常的緣由就是,編譯器會將 finally 塊中的代碼複製兩份並分別添加在 try 和 catch 的後面

可能有人會所疑惑,本來咱們的 i 就被存儲在局部變量表 0 位置,而最後 finally 中的代碼也的確將 slot 0 位置填充了數值 10,可爲何最後程序依然返回的數值 2 呢?

仔細看字節碼,你會發如今 return 語句返回以前,虛擬機會將待返回的值壓入操做數棧,等待返回,即便 finally 語句塊對 i 進行了修改,可是待返回的值已經確實的存在於操做數棧中了,因此不會影響程序返回結果。

面試題三:

public static int test4() {
    //finally 語句塊中有 return 語句
    int i = 1;
    try {
        i++;
        System.out.println("try block, i = " + i);
        return i;
    } catch (Exception e) {
        i++;
        System.out.println("catch block i = " + i);
        return i;
    } finally {
        i++;
        System.out.println("finally block i = " + i);
        return i;
    }
}

運行結果:

try block, i = 2
finally block i = 3
3

其實你從它的字節碼指令去看整個過程,而不要單單死記它的執行過程。

你會發現程序最終會採用 finally 代碼塊中的 return 語句進行返回,而直接忽略 try 語句塊中的 return 指令。

自定義異常

Java 的異常機制中所定義的全部異常不可能預見全部可能出現的錯誤,某些特定的情境下,則須要咱們自定義異常類型來向上報告某些錯誤信息。

而自定義異常類型也是至關簡單的,你能夠選擇繼承 Throwable,Exception 或它們的子類,甚至你不須要實現和重寫父類的任何方法便可完成一個異常類型的定義。

例如:

public class MyException extends RuntimeException{ }
public class MyException extends Exception{ }

按照國際慣例,自定義的異常應該老是包含以下的構造函數:

  • 一個無參構造函數
  • 一個帶有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);
    }
}

異常的注意事項

一、當子類重寫父類的帶有 throws聲明的函數時,其throws聲明的異常必須在父類異常的可控範圍內——用於處理父類的throws方法的異常處理器,必須也適用於子類的這個帶throws方法 。這是爲了支持多態。

例如,父類方法throws 的是2個異常,子類就不能throws 3個及以上的異常。父類throws IOException,子類就必須throws IOException或者IOException的子類。

二、Java程序能夠是多線程的。每個線程都是一個獨立的執行流,獨立的函數調用棧。若是程序只有一個線程,那麼沒有被任何代碼處理的異常 會致使程序終止。若是是多線程的,那麼沒有被任何代碼處理的異常僅僅會致使異常所在的線程結束。

也就是說,Java中的異常是線程獨立的,線程的問題應該由線程本身來解決,而不要委託到外部,也不會直接影響到其它線程的執行。

異常使用時的常見錯誤

一、將異常直接顯示在頁面或客戶端

將異常直接打印在客戶端的例子家常便飯,一旦程序運行出現異常,默認狀況下容器將異常堆棧信息直接打印在頁面上。從客戶角度來講,任何異常都沒有實際意義,絕大多數的客戶也根本看不懂異常信息,軟件開發也要儘可能避免將異常直接呈現給用戶,必定要在前端展現層對異常進行封裝後展現。目前絕大多數應用都是先後端分離的模式,這種直接打印異常的狀況已經相對改善了不少,不過咱們在編碼時仍是要特別注意下這個原則。

二、忽略異常

以下異常處理只是將異常輸出到控制檯,沒有任何意義。並且這裏出現了異常並無中斷程序,進而調用代碼繼續執行,致使更多的異常。

public void retrieveObjectById(Long id) {
    try {
        //..some code that throws SQLException
    } catch (SQLException ex) {
        /**
          *瞭解的人都知道,這裏的異常打印毫無心義,僅僅是將錯誤堆棧輸出到控制檯。
          * 而在 Production 環境中,須要將錯誤堆棧輸出到日誌。
          * 並且這裏 catch 處理以後程序繼續執行,會致使進一步的問題*/

        ex.printStacktrace();
    }
}

捕獲了異常缺不進行處理,這是咱們在寫代碼時候的大忌,能夠重構成:

public void retrieveObjectById(Long id) {
    try {
        //..some code that throws SQLException
    } catch (SQLException ex) {
        throw new RuntimeException("Exception in retieveObjectById」, ex);
    } finally {
        //clean up resultset, statement, connection etc
    }
}
三、將異常包含在循環語句塊中

以下代碼所示,異常包含在 for 循環語句塊中。

for (int i = 0; i < 100; i++) {
    try {
    } catch (XXXException e) {
        //....
    }
}

咱們都知道異常處理佔用系統資源。一看,你們都認爲不會犯這樣的錯誤。換個角度,類 A 中執行了一段循環,循環中調用了 B 類的方法,B 類中被調用的方法卻又包含 try-catch 這樣的語句塊。褪去類的層次結構,代碼和上面一模一樣。

四、利用 Exception 捕捉全部潛在的異常

一段方法執行過程當中拋出了幾個不一樣類型的異常,爲了代碼簡潔,利用基類 Exception 捕捉全部潛在的異常,以下例所示:

public void retrieveObjectById(Long id) {
    try {
        //...拋出 IOException 的代碼調用
        //...拋出 SQLException 的代碼調用
    } catch (Exception e) {
        //這裏利用基類 Exception 捕捉的全部潛在的異常,若是多個層次這樣捕捉,會丟失原始異常的有效信息
        throw new RuntimeException("Exception in retieveObjectById」, e);
    }
}

估計大部分程序員都會有這種寫法,爲了省事簡便,直接一個頂層的exception來捕獲全部可能出現的異常,這樣雖然能夠保證異常確定會被捕捉到,可是程序卻沒法針對不一樣的錯誤異常進行對應正確的處理,能夠重構成:

public void retrieveObjectById(Long id) {
    try {
        //..some code that throws RuntimeException, IOException, SQLException
    } catch (IOException e) {
        //僅僅捕捉 IOException
        throw new RuntimeException(/*指定這裏 IOException 對應的錯誤代碼*/code, "Exception in retieveObjectById」, e);
    } catch (SQLException e) {
        //僅僅捕捉 SQLException
        throw new RuntimeException(/*指定這裏 SQLException 對應的錯誤代碼*/code, "Exception in retieveObjectById」, e);
    }
}
五、異常包含的信息不能充分定位問題

異常不只要可以讓開發人員知道哪裏出了問題,更多時候開發人員還須要知道是什麼緣由致使的問題,咱們知道 java .lang.Exception 有字符串類型參數的構造方法,這個字符串能夠自定義成通俗易懂的提示信息。

簡單的自定義信息開發人員只能知道哪裏出現了異常,可是不少的狀況下,開發人員更須要知道是什麼參數致使了這樣的異常。這個時候咱們就須要將方法調用的參數信息追加到自定義信息中。下例只列舉了一個參數的狀況,多個參數的狀況下,能夠單獨寫一個工具類組織這樣的字符串。

public void retieveObjectById(Long id) {
    try {
        //..some code that throws SQLException
    } catch (SQLException ex) {
        //將參數信息添加到異常信息中
        throw new RuntimeException("Exception in retieveObjectById with Object Id :"+ id, ex);
    }
}

總結

異常做爲Java語言中重要的錯誤處理機制,也做爲查找程序緣由,提高程序健壯性,前端產品良好體驗的重要保障,因此掌握異常的使用是很是有必要的,但願本文能對你們有所幫助,若是有什麼疑問或問題歡迎隨時騷擾。

創做不易,若是你們喜歡本文,歡迎點贊,轉發,你的關注是咱們繼續前進的動力 ^_^

參考

技術人,技術魂,天天肝一篇技術文,ヾ(◍°∇°◍)ノ゙哈哈~

關於做者
聯繫做者
  • 微信號:ForTheDeveloper
  • 公衆號:ForTheDevelopers
相關文章
相關標籤/搜索