深入理解 Java 異常

📓 本文已歸檔到:「javacore

🔁 本文中的示例代碼已歸檔到:「javacore

1. 異常框架

1.1. Throwable

Throwable 是 Java 語言中所有錯誤(Error)和異常(Exception)的超類。

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

主要方法:

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

1.2. Error

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

常見 Error

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

1.3. Exception

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

編譯器會檢查 Exception 異常。此類異常,要麼通過 throws 進行聲明拋出,要麼通過 try catch 進行捕獲處理,否則不能通過編譯。

常見 Exception

  • 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; 必須對其進行捕獲或聲明以便拋出

1.4. 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 - 當不支持請求的操作時,拋出該異常。

2. 自定義異常

自定義一個異常類,只需要繼承 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)

3. 拋出異常

如果想在程序中明確地拋出異常,需要用到 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 後面跟的是異常對象。

4. 捕獲異常

使用 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
不管是否出現異常,都執行此代碼

5. 異常鏈

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

通過使用異常鏈,我們可以提高代碼的可理解性、系統的可維護性和友好性。

我們有兩種方式處理異常,一是 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

擴展閱讀:https://juejin.im/post/5b6d61e55188251b38129f9a#heading-10

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

6. 異常注意事項

6.1. 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

6.2. 覆蓋拋出異常的方法

當子類重寫父類帶有 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 中的異常了。多態就不能實現了。

6.3. 異常和線程

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

7. 最佳實踐

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

      擴展閱讀:

      - Effective java 中文版 之 第九章 異常

      - 優雅的處理你的 Java 異常

8. 小結

img

img

9. 參考資料

發佈了182 篇原創文章 · 獲贊 32 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章