深刻理解 Java 異常

:notebook: 本文已歸檔到:「bloghtml

:keyboard: 本文中的示例代碼已歸檔到:「javacorejava

異常框架



Throwable

Throwable 是 Java 語言中全部錯誤(Error)和異常(Exception)的超類。git

Throwable 包含了其線程建立時線程執行堆棧的快照,它提供了 printStackTrace() 等接口用於獲取堆棧跟蹤數據等信息。程序員

主要方法:github

  • fillInStackTrace - 用當前的調用棧層次填充 Throwable 對象棧層次,添加到棧層次任何先前信息中。
  • getMessage - 返回關於發生的異常的詳細信息。這個消息在 Throwable 類的構造函數中初始化了。
  • getCause - 返回一個 Throwable 對象表明異常緣由。
  • getStackTrace - 返回一個包含堆棧層次的數組。下標爲 0 的元素表明棧頂,最後一個元素表明方法調用堆棧的棧底。
  • printStackTrace - 打印 toString() 結果和棧層次到 System.err,即錯誤輸出流。
  • toString - 使用 getMessage 的結果返回表明 Throwable 對象的字符串。

Error

ErrorThrowable 的一個子類。Error 表示合理的應用程序不該該嘗試捕獲的嚴重問題。大多數此類錯誤都是異常狀況。編譯器不會檢查 Error數據庫

常見 Error編程

  • AssertionError - 斷言錯誤。
  • VirtualMachineError - 虛擬機錯誤。
  • UnsupportedClassVersionError - Java 類版本錯誤。
  • StackOverflowError - 棧溢出錯誤。
  • OutOfMemoryError - 內存溢出錯誤。

Exception

ExceptionThrowable 的一個子類。Exception 表示合理的應用程序可能想要捕獲的條件。數組

**編譯器會檢查 Exception 異常。**此類異常,要麼經過 throws 進行聲明拋出,要麼經過 try catch 進行捕獲處理,不然不能經過編譯。安全

常見 Exceptionbash

  • ClassNotFoundException - 應用程序試圖加載類時,找不到相應的類,拋出該異常。
  • CloneNotSupportedException - 當調用 Object 類中的 clone 方法克隆對象,但該對象的類沒法實現 Cloneable 接口時,拋出該異常。
  • IllegalAccessException - 拒絕訪問一個類的時候,拋出該異常。
  • InstantiationException - 當試圖使用 Class 類中的 newInstance 方法建立一個類的實例,而指定的類對象由於是一個接口或是一個抽象類而沒法實例化時,拋出該異常。
  • InterruptedException - 一個線程被另外一個線程中斷,拋出該異常。
  • NoSuchFieldException - 請求的變量不存在。
  • NoSuchMethodException - 請求的方法不存在。

示例:

public class ExceptionDemo {
    public static void main(String[] args) {
        Method method = String.class.getMethod("toString", int.class);
    }
};
複製代碼

試圖編譯運行時會報錯:

Error:(7, 47) java: 未報告的異常錯誤java.lang.NoSuchMethodException; 必須對其進行捕獲或聲明以便拋出
複製代碼

RuntimeException

RuntimeExceptionException 的一個子類。RuntimeException 是那些可能在 Java 虛擬機正常運行期間拋出的異常的超類。

**編譯器不會檢查 RuntimeException 異常。**當程序中可能出現這類異常時,假若既沒有經過 throws 聲明拋出它,也沒有用 try catch 語句捕獲它,程序仍是會編譯經過。

示例:

public class RuntimeExceptionDemo {
    public static void main(String[] args) {
        // 此處產生了異常
        int result = 10 / 0;
        System.out.println("兩個數字相除的結果:" + result);
        System.out.println("----------------------------");
    }
};
複製代碼

運行時輸出:

Exception in thread "main" java.lang.ArithmeticException: / by zero
	at io.github.dunwu.javacore.exception.RumtimeExceptionDemo01.main(RumtimeExceptionDemo01.java:6)
複製代碼

常見 RuntimeException

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

自定義異常

自定義一個異常類,只須要繼承 ExceptionRuntimeException 便可。

示例:

public class MyExceptionDemo {
    public static void main(String[] args) {
        throw new MyException("自定義異常");
    }

    static class MyException extends RuntimeException {
        public MyException(String message) {
            super(message);
        }
    }
}
複製代碼

輸出:

Exception in thread "main" io.github.dunwu.javacore.exception.MyExceptionDemo$MyException: 自定義異常
	at io.github.dunwu.javacore.exception.MyExceptionDemo.main(MyExceptionDemo.java:9)
複製代碼

拋出異常

若是想在程序中明確地拋出異常,須要用到 throwthrows

若是一個方法沒有捕獲一個檢查性異常,那麼該方法必須使用 throws 關鍵字來聲明。throws 關鍵字放在方法簽名的尾部。

throw 示例:

public class ThrowDemo {
    public static void f() {
        try {
            throw new RuntimeException("拋出一個異常");
        } catch (Exception e) {
            System.out.println(e);
        }
    }

    public static void main(String[] args) {
        f();
    }
};
複製代碼

輸出:

java.lang.RuntimeException: 拋出一個異常
複製代碼

也可使用 throw 關鍵字拋出一個異常,不管它是新實例化的仍是剛捕獲到的。

throws 示例:

public class ThrowsDemo {
    public static void f1() throws NoSuchMethodException, NoSuchFieldException {
        Field field = Integer.class.getDeclaredField("digits");
        if (field != null) {
            System.out.println("反射獲取 digits 方法成功");
        }
        Method method = String.class.getMethod("toString", int.class);
        if (method != null) {
            System.out.println("反射獲取 toString 方法成功");
        }
    }

    public static void f2() {
        try {
            // 調用 f1 處,若是不用 try catch ,編譯時會報錯
            f1();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        f2();
    }
};
複製代碼

輸出:

反射獲取 digits 方法成功
java.lang.NoSuchMethodException: java.lang.String.toString(int)
	at java.lang.Class.getMethod(Class.java:1786)
	at io.github.dunwu.javacore.exception.ThrowsDemo.f1(ThrowsDemo.java:12)
	at io.github.dunwu.javacore.exception.ThrowsDemo.f2(ThrowsDemo.java:21)
	at io.github.dunwu.javacore.exception.ThrowsDemo.main(ThrowsDemo.java:30)
複製代碼

throw 和 throws 的區別:

  • throws 使用在函數上,throw 使用在函數內。
  • throws 後面跟異常類,能夠跟多個,用逗號區別;throw 後面跟的是異常對象。

捕獲異常

使用 try 和 catch 關鍵字能夠捕獲異常。try catch 代碼塊放在異常可能發生的地方。

它的語法形式以下:

try {
    // 可能會發生異常的代碼塊
} catch (Exception e1) {
    // 捕獲並處理try拋出的異常類型Exception
} catch (Exception2 e2) {
    // 捕獲並處理try拋出的異常類型Exception2
} finally {
    // 不管是否發生異常,都將執行的代碼塊
}
複製代碼

此外,JDK7 之後,catch 多種異常時,也能夠像下面這樣簡化代碼:

try {
    // 可能會發生異常的代碼塊
} catch (Exception | Exception2 e) {
    // 捕獲並處理try拋出的異常類型
} finally {
    // 不管是否發生異常,都將執行的代碼塊
}
複製代碼
  • try - try 語句用於監聽。將要被監聽的代碼(可能拋出異常的代碼)放在 try 語句塊以內,當 try 語句塊內發生異常時,異常就被拋出。
  • catch - catch 語句包含要捕獲異常類型的聲明。當保護代碼塊中發生一個異常時,try 後面的 catch 塊就會被檢查。
  • finally - finally 語句塊老是會被執行,不管是否出現異常。try catch 語句後不必定非要finally 語句。finally 經常使用於這樣的場景:因爲finally 語句塊老是會被執行,因此那些在 try 代碼塊中打開的,而且必須回收的物理資源(如數據庫鏈接、網絡鏈接和文件),通常會放在finally 語句塊中釋放資源。
  • trycatchfinally 三個代碼塊中的局部變量不可共享使用。
  • catch 塊嘗試捕獲異常時,是按照 catch 塊的聲明順序從上往下尋找的,一旦匹配,就不會再向下執行。所以,若是同一個 try 塊下的多個 catch 異常類型有父子關係,應該將子類異常放在前面,父類異常放在後面。

示例:

public class TryCatchFinallyDemo {
    public static void main(String[] args) {
        try {
            // 此處產生了異常
            int temp = 10 / 0;
            System.out.println("兩個數字相除的結果:" + temp);
            System.out.println("----------------------------");
        } catch (ArithmeticException e) {
            System.out.println("出現異常了:" + e);
        } finally {
            System.out.println("不論是否出現異常,都執行此代碼");
        }
    }
};
複製代碼

運行時輸出:

出現異常了:java.lang.ArithmeticException: / by zero
不論是否出現異常,都執行此代碼
複製代碼

異常鏈

異常鏈是以一個異常對象爲參數構造新的異常對象,新的異常對象將包含先前異常的信息。

經過使用異常鏈,咱們能夠提升代碼的可理解性、系統的可維護性和友好性。

咱們有兩種方式處理異常,一是 throws 拋出交給上級處理,二是 try…catch 作具體處理。try…catch 的 catch 塊咱們能夠不須要作任何處理,僅僅只用 throw 這個關鍵字將咱們封裝異常信息主動拋出來。而後在經過關鍵字 throws 繼續拋出該方法異常。它的上層也能夠作這樣的處理,以此類推就會產生一條由異常構成的異常鏈。

示例:

public class ExceptionChainDemo {
    static class MyException1 extends Exception {
        public MyException1(String message) {
            super(message);
        }
    }

    static class MyException2 extends Exception {
        public MyException2(String message, Throwable cause) {
            super(message, cause);
        }
    }

    public static void f1() throws MyException1 {
        throw new MyException1("出現 MyException1");
    }

    public static void f2() throws MyException2 {
        try {
            f1();
        } catch (MyException1 e) {
            throw new MyException2("出現 MyException2", e);
        }
    }

    public static void main(String[] args) throws MyException2 {
        f2();
    }
}
複製代碼

輸出:

Exception in thread "main" io.github.dunwu.javacore.exception.ExceptionChainDemo$MyException2: 出現 MyException2
	at io.github.dunwu.javacore.exception.ExceptionChainDemo.f2(ExceptionChainDemo.java:29)
	at io.github.dunwu.javacore.exception.ExceptionChainDemo.main(ExceptionChainDemo.java:34)
Caused by: io.github.dunwu.javacore.exception.ExceptionChainDemo$MyException1: 出現 MyException1
	at io.github.dunwu.javacore.exception.ExceptionChainDemo.f1(ExceptionChainDemo.java:22)
	at io.github.dunwu.javacore.exception.ExceptionChainDemo.f2(ExceptionChainDemo.java:27)
	... 1 more
複製代碼

擴展閱讀:juejin.im/post/5b6d61…

這篇文章中對於異常鏈講解比較詳細。

異常注意事項

finally 覆蓋異常

Java 異常處理中 finally 中的 return 會覆蓋 catch 代碼塊中的 return 語句和 throw 語句,因此 Java 不建議在 finally 中使用 return 語句

此外 finally 中的 throw 語句也會覆蓋 catch 代碼塊中的 return 語句和 throw 語句。

示例:

public class FinallyOverrideExceptionDemo {
    static void f() throws Exception {
        try {
            throw new Exception("A");
        } catch (Exception e) {
            throw new Exception("B");
        } finally {
            throw new Exception("C");
        }
    }

    public static void main(String[] args) {
        try {
            f();
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }
}
複製代碼

輸出:C

覆蓋拋出異常的方法

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

示例:

public class ExceptionOverrideDemo {
    static class Father {
        public void start() throws IOException {
            throw new IOException();
        }
    }

    static class Son extends Father {
        @Override
        public void start() throws SQLException {
            throw new SQLException();
        }
    }

    public static void main(String[] args) {
        Father obj1 = new Father();
        Father obj2 = new Son();
        try {
            obj1.start();
            obj2.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
複製代碼

上面的示例編譯時會報錯,緣由在於:

由於 Son 類拋出異常的實質是 SQLException,而 IOException 沒法處理它。那麼這裏的 try catch 就不能處理 Son 中的異常了。多態就不能實現了。

異常和線程

若是 Java 程序只有一個線程,那麼沒有被任何代碼處理的異常會致使程序終止。若是 Java 程序是多線程的,那麼沒有被任何代碼處理的異常僅僅會致使異常所在的線程結束。

最佳實踐

  • 對可恢復的狀況使用檢查性異常(Exception),對編程錯誤使用運行時異常(RuntimeException)
  • 優先使用 Java 標準的異常
  • 拋出與抽象相對應的異常
  • 在細節消息中包含能捕獲失敗的信息
  • 儘量減小 try 代碼塊的大小
  • 儘可能縮小異常範圍。例如,若是明知嘗試捕獲的是一個 ArithmeticException,就應該 catch ArithmeticException,而不是 catch 範圍較大的 RuntimeException,甚至是 Exception
  • 儘可能不要在 finally 塊拋出異常或者返回值
  • 不要忽略異常,一旦捕獲異常,就應該處理,而非丟棄
  • 異常處理效率很低,因此不要用異常進行業務邏輯處理
  • 各種異常必需要有單獨的日誌記錄,將異常分級,分類管理,由於有的時候僅僅想給第三方運維看到邏輯異常,而不是更細節的信息。
  • 如何對異常進行分類
    • 邏輯異常,這類異經常使用於描述業務沒法按照預期的狀況處理下去,屬於用戶製造的意外。
    • 代碼錯誤,這類異經常使用於描述開發的代碼錯誤,例如 NPE,ILLARG,都屬於程序員製造的 BUG。
    • 專有異常,多用於特定業務場景,用於描述指定做業出現意外狀況沒法預先處理。

擴展閱讀:

小結





參考資料

相關文章
相關標籤/搜索