亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

Java 異常處理

Java 的異常處理是 Java 語言的一大重要特性,也是提高代碼健壯性的最強大方法之一。當我們編寫了錯誤的代碼時,編譯器在編譯期間可能會拋出異常,有時候即使編譯正常,在運行代碼的時候也可能會拋出異常。本小節我們將介紹什么是異常、Java 中異常類的架構如何進行異常處理、如何自定義異常什么是異常鏈、如何使用異常鏈等內容。

1. 什么是異常

異常就是程序上的錯誤,我們在編寫程序的時候經常會產生錯誤,這些錯誤劃分為編譯期間的錯誤運行期間的錯誤。

下面我們來看幾個常見的異常案例。

如果語句漏寫分號,程序在編譯期間就會拋出異常,實例如下:

public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello World!")
    }
}

運行結果:

$ javac Hello.java
Hello.java:3: 錯誤: 需要';'
        System.out.println("Hello World!")
                                          ^
1 個錯誤

運行過程:

由于代碼的第 3 行語句漏寫了分號,Java 編譯器給出了明確的提示。

static 關鍵字寫成了 statci,實例如下:

Hello.java:2: 錯誤: 需要<標識符>
    public statci void main(String[] args) {
                 ^
1 個錯誤

當數組下標越界,程序在編譯階段不會發生錯誤,但在運行時會拋出異常。實例如下:

public class ArrayOutOfIndex {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3};
        System.out.println(arr[3]);
    }
}

運行結果:

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
	at ArrayOutOfIndex.main(ArrayOutOfIndex.java:4)

運行過程:

2. Java 異常類架構

在 Java 中,通過 Throwable 及其子類來描述各種不同類型的異常。如下是 Java 異常類的架構圖(不是全部,只展示部分類):

2.1 Throwable 類

Throwable 位于 java.lang 包下,它是 Java 語言中所有錯誤(Error)和異常(Exception)的父類。

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

主要方法:

  • fillInStackTrace: 用當前的調用棧層次填充 Throwable 對象棧層次,添加到棧層次任何先前信息中;

  • getMessage:返回關于發生的異常的詳細信息。這個消息在 Throwable 類的構造函數中初始化了;

  • getCause:返回一個 Throwable 對象代表異常原因;

  • getStackTrace:返回一個包含堆棧層次的數組。下標為 0 的元素代表棧頂,最后一個元素代表方法調用堆棧的棧底;

  • printStackTrace:打印 toString() 結果和棧層次到 System.err,即錯誤輸出流。

2.2 Error 類

ErrorThrowable 的一個直接子類,它可以指示合理的應用程序不應該嘗試捕獲的嚴重問題。這些錯誤在應用程序的控制和處理能力之外,編譯器不會檢查 Error,對于設計合理的應用程序來說,即使發生了錯誤,本質上也無法通過異常處理來解決其所引起的異常狀況。

常見 Error

  • AssertionError:斷言錯誤;

  • VirtualMachineError:虛擬機錯誤;

  • UnsupportedClassVersionError:Java 類版本錯誤;

  • OutOfMemoryError :內存溢出錯誤。

2.3 Exception 類

ExceptionThrowable 的一個直接子類。它指示合理的應用程序可能希望捕獲的條件。

Exception 又包括 Unchecked Exception(非檢查異常)和 Checked Exception(檢查異常)兩大類別。

2.3.1 Unchecked Exception (非檢查異常)

Unchecked Exception 是編譯器不要求強制處理的異常,包含 RuntimeException 以及它的相關子類。我們編寫代碼時即使不去處理此類異常,程序還是會編譯通過。

常見非檢查異常:

  • NullPointerException:空指針異常;

  • ArithmeticException:算數異常;

  • ArrayIndexOutOfBoundsException:數組下標越界異常;

  • ClassCastException:類型轉換異常。

2.3.2 Checked Exception(檢查異常)

Checked Exception 是編譯器要求必須處理的異常,除了 RuntimeException 以及它的子類,都是 Checked Exception 異常。我們在程序編寫時就必須處理此類異常,否則程序無法編譯通過。

常見檢查異常:

  • IOException:IO 異常

  • SQLException:SQL 異常

3. 如何進行異常處理

在 Java 語言中,異常處理機制可以分為兩部分:

  1. 拋出異常:當一個方法發生錯誤時,會創建一個異常對象,并交給運行時系統處理;

  2. 捕獲異常:在方法拋出異常之后,運行時系統將轉為尋找合適的異常處理器。

Java 通過 5 個關鍵字來實現異常處理,分別是:throw、throws、try、catch、finally。

異常總是先拋出,后捕獲的。下面我們將圍繞著 5 個關鍵字來詳細講解如何拋出異常以及如何捕獲異常。

4. 拋出異常

4.1 實例

我們先來看一個除零異常的實例代碼:

public class ExceptionDemo1 {
    // 打印 a / b 的結果
    public static void divide(int a, int b) {
        System.out.println(a / b);
    }

    public static void main(String[] args) {
        // 調用 divide() 方法
        divide(2, 0);
    }
}

運行結果:

Exception in thread "main" java.lang.ArithmeticException: / by zero
	at ExceptionDemo1.divide(ExceptionDemo1.java:4)
	at ExceptionDemo1.main(ExceptionDemo1.java:9)

運行過程:

我們知道 0 是不能用作除數的,由于 divide() 方法中除數 b0,所以代碼將停止執行并顯示了相關的異常信息,此信息為堆棧跟蹤,上面的運行結果告訴我們:main 線程發生了類型為 ArithmeticException 的異常,顯示消息為 by zero,并且提示了可能發生異常的方法和行號。

4.2 throw

上面的實例中,程序在運行時引發了錯誤,那么如何來顯示拋出(創建)異常呢?

我們可以使用 throw 關鍵字來拋出異常,throw 關鍵字后面跟異常對象,改寫上面的實例代碼:

public class ExceptionDemo2 {
    // 打印 a / b 的結果
    public static void divide(int a, int b) {
        if (b == 0) {
            // 拋出異常
            throw new ArithmeticException("除數不能為零");
        }
        System.out.println(a / b);
    }

    public static void main(String[] args) {
        // 調用 divide() 方法
        divide(2, 0);
    }
}

運行結果:

Exception in thread "main" java.lang.ArithmeticException: 除數不能為零
	at ExceptionDemo2.divide(ExceptionDemo2.java:5)
	at ExceptionDemo2.main(ExceptionDemo2.java:12)

運行過程:

代碼在運行時同樣引發了錯誤,但顯示消息為 “除數不能為零”。我們看到 divide() 方法中加入了條件判斷,如果調用者將參數 b 設置為 0 時,會使用 throw 關鍵字來拋出異常,throw 后面跟了一個使用 new 關鍵字實例化的算數異常對象,并且將消息字符串作為參數傳遞給了算數異常的構造函數。

我們可以使用 throw 關鍵字拋出任何類型的 Throwable 對象,它會中斷方法,throw 語句之后的所有內容都不會執行。除非已經處理拋出的異常。異常對象不是從方法中返回的,而是從方法中拋出的。

4.3 throws

可以通過 throws 關鍵字聲明方法要拋出何種類型的異常。如果一個方法可能會出現異常,但是沒有能力處理這種異常,可以在方法聲明處使用 throws 關鍵字來聲明要拋出的異常。例如,汽車在運行時可能會出現故障,汽車本身沒辦法處理這個故障,那就讓開車的人來處理。

throws 用在方法定義時聲明該方法要拋出的異常類型,如下是偽代碼:

public void demoMethod() throws Exception1, Exception2, ... ExceptionN {
    // 可能產生異常的代碼
}

throws 后面跟的異常類型列表可以有一個也可以有多個,多個則以 , 分割。當方法產生異常列表中的異常時,將把異常拋向方法的調用方,由調用方處理。

throws 有如下使用規則:

  1. 如果方法中全部是非檢查異常(即 Error、RuntimeException 以及的子類),那么可以不使用 throws 關鍵字來聲明要拋出的異常,編譯器能夠通過編譯,但在運行時會被系統拋出;
  2. 如果方法中可能出現檢查異常,就必須使用 throws 聲明將其拋出或使用 try catch 捕獲異常,否則將導致編譯錯誤;
  3. 當一個方法拋出了異常,那么該方法的調用者必須處理或者重新拋出該異常;
  4. 當子類重寫父類拋出異常的方法時,聲明的異常必須是父類所聲明異常的同類或子類。

5. 捕獲異常

使用 try 和 catch 關鍵字可以捕獲異常。try catch 代碼塊放在異常可能發生的地方。它的語法如下:

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

我們來看一下上面語法中的 3 種語句塊:

  1. try 語句塊:用于監聽異常,當發生異常時,異常就會被拋出;
  2. catch 語句塊catch 語句包含要捕獲的異常類型的聲明,當 try 語句塊發生異常時,catch 語句塊就會被檢查。當 catch 塊嘗試捕獲異常時,是按照 catch 塊的聲明順序從上往下尋找的,一旦匹配,就不會再向下執行。因此,如果同一個 try 塊下的多個 catch 異常類型有父子關系,應該將子類異常放在前面,父類異常放在后面;
  3. finally 語句塊:無論是否發生異常,都會執行 finally 語句塊。finally 常用于這樣的場景:由于 finally 語句塊總是會被執行,所以那些在 try 代碼塊中打開的,并且必須回收的物理資源(如數據庫連接、網絡連接和文件),一般會放在 finally 語句塊中釋放資源。

try 語句塊后可以接零個或多個 catch 語句塊,如果沒有 catch 塊,則必須跟一個 finally 語句塊。簡單來說,try 不允許單獨使用,必須和 catchfinally 組合使用,catchfinally 也不能單獨使用。

實例如下:

public class ExceptionDemo3 {
    // 打印 a / b 的結果
    public static void divide(int a, int b) {
        System.out.println(a / b);
    }

    public static void main(String[] args) {
        try {
            // try 語句塊
            // 調用 divide() 方法
            divide(2, 0);
        } catch (ArithmeticException e) {
            // catch 語句塊
            System.out.println("catch: 發生了算數異常:" + e);
        } finally {
            // finally 語句塊
            System.out.println("finally: 無論是否發生異常,都會執行");
        }
    }
}

運行結果:

catch: 發生了算數異常:java.lang.ArithmeticException: / by zero
finally: 無論是否發生異常,都會執行

運行過程:

divide() 方法中除數 b0,會發生除零異常,我們在方法調用處使用了 try 語句塊對異常進行捕獲;如果捕獲到了異常, catch 語句塊會對 ArithmeticException 類型的異常進行處理,此處打印了一行自定義的提示語句;最后的 finally 語句塊,無論發生異常與否,總會執行。

Java 7 以后,catch 多種異常時,也可以像下面這樣簡化代碼:

try {
    // 可能會發生異常的代碼塊
} catch (Exception | Exception2 e) {
    // 捕獲并處理try拋出的異常類型
} finally {
    // 無論是否發生異常,都將執行的代碼塊
}

6. 自定義異常

自定義異常,就是定義一個類,去繼承 Throwable 類或者它的子類。

Java 內置了豐富的異常類,通常使用這些內置異常類,就可以描述我們在編碼時出現的大部分異常情況。一旦內置異常無法滿足我們的業務要求,就可以通過自定義異常描述特定業務產生的異常類型。

實例:

public class ExceptionDemo4 {

    static class MyCustomException extends RuntimeException {
        /**
         * 無參構造方法
         */
        public MyCustomException() {
            super("我的自定義異常");
        }
    }

    public static void main(String[] args) {
      	// 直接拋出異常
        throw new MyCustomException();
    }
}

運行結果:

Exception in thread "main" ExceptionDemo4$MyCustomException: 我的自定義異常
	at ExceptionDemo4.main(ExceptionDemo4.java:13)

運行過程:

在代碼中寫了一個自定義異常 MyCustomException,繼承自 RuntimeException,它是一個靜態內部類,這樣在主方法中就可以直接拋出這個異常類了。當然,也可以使用 catch 來捕獲此類型異常。

7. 異常鏈

異常鏈是以一個異常對象為參數構造新的異常對象,新的異常對象將包含先前異常的信息。簡單來說,就是將異常信息從底層傳遞給上層,逐層拋出,我們來看一個實例:

public class ExceptionDemo5 {

    /**
     * 第一個自定義的靜態內部異常類
     */
    static class FirstCustomException extends Exception {

        // 無參構造方法
        public FirstCustomException() {
            super("第一個異常");
        }
    }

    /**
     * 第二個自定義的靜態內部異常類
     */
    static class SecondCustomException extends Exception {

        public SecondCustomException() {
            super("第二個異常");
        }
    }

    /**
     * 第三個自定義的靜態內部異常類
     */
    static class ThirdCustomException extends Exception {

        public ThirdCustomException() {
            super("第三個異常");
        }
    }

    /**
     * 測試異常鏈靜態方法1,直接拋出第一個自定義的靜態內部異常類
     * @throws FirstCustomException
     */
    public static void f1() throws FirstCustomException {
        throw new FirstCustomException();
    }

    /**
     * 測試異常鏈靜態方法2,調用f1()方法,并拋出第二個自定義的靜態內部異常類
     * @throws SecondCustomException
     */
    public static void f2() throws SecondCustomException {
        try {
            f1();
        } catch (FirstCustomException e) {
            throw new SecondCustomException();
        }
    }

    /**
     * 測試異常鏈靜態方法3,調用f2()方法, 并拋出第三個自定義的靜態內部異常類
     * @throws ThirdCustomException
     */
    public static void f3() throws ThirdCustomException {
        try {
            f2();
        } catch (SecondCustomException e) {
            throw new ThirdCustomException();
        }
    }

    public static void main(String[] args) throws ThirdCustomException {
        // 調用靜態方法f3()
        f3();
    }
}

運行結果:

Exception in thread "main" ExceptionDemo5$ThirdCustomException: 第三個異常
	at ExceptionDemo5.f3(ExceptionDemo5.java:46)
	at ExceptionDemo5.main(ExceptionDemo5.java:51)

運行過程:

通過運行結果,我們只獲取到了靜態方法 f3() 所拋出的異常堆棧信息,前面代碼所拋出的異常并沒有被顯示。

我們改寫上面的代碼,讓異常信息以鏈條的方式 “連接” 起來。可以通過改寫自定義異常的構造方法,來獲取到之前異常的信息。實例如下:

/**
 * @author colorful@TaleLin
 */
public class ExceptionDemo6 {

    /**
     * 第一個自定義的靜態內部異常類
     */
    static class FirstCustomException extends Exception {

        // 無參構造方法
        public FirstCustomException() {
            super("第一個異常");
        }

    }

    /**
     * 第二個自定義的靜態內部異常類
     */
    static class SecondCustomException extends Exception {

        /**
         * 通過構造方法獲取之前異常的信息
         * @param cause 捕獲到的異常對象
         */
        public SecondCustomException(Throwable cause) {
            super("第二個異常", cause);
        }
    }

    /**
     * 第三個自定義的靜態內部異常類
     */
    static class ThirdCustomException extends Exception {

        /**
         * 通過構造方法獲取之前異常的信息
         * @param cause 捕獲到的異常對象
         */
        public ThirdCustomException(Throwable cause) {
            super("第三個異常", cause);
        }
    }

    /**
     * 測試異常鏈靜態方法1,直接拋出第一個自定義的靜態內部異常類
     * @throws FirstCustomException
     */
    public static void f1() throws FirstCustomException {
        throw new FirstCustomException();
    }

    /**
     * 測試異常鏈靜態方法2,調用f1()方法,并拋出第二個自定義的靜態內部異常類
     * @throws SecondCustomException
     */
    public static void f2() throws SecondCustomException {
        try {
            f1();
        } catch (FirstCustomException e) {
            throw new SecondCustomException(e);
        }
    }

    /**
     * 測試異常鏈靜態方法3,調用f2()方法, 并拋出第三個自定義的靜態內部異常類
     * @throws ThirdCustomException
     */
    public static void f3() throws ThirdCustomException {
        try {
            f2();
        } catch (SecondCustomException e) {
            throw new ThirdCustomException(e);
        }
    }

    public static void main(String[] args) throws ThirdCustomException {
        // 調用靜態方法f3()
        f3();
    }
}

運行結果:

Exception in thread "main" ExceptionDemo6$ThirdCustomException: 第三個異常
	at ExceptionDemo6.f3(ExceptionDemo6.java:74)
	at ExceptionDemo6.main(ExceptionDemo6.java:80)
Caused by: ExceptionDemo6$SecondCustomException: 第二個異常
	at ExceptionDemo6.f2(ExceptionDemo6.java:62)
	at ExceptionDemo6.f3(ExceptionDemo6.java:72)
	... 1 more
Caused by: ExceptionDemo6$FirstCustomException: 第一個異常
	at ExceptionDemo6.f1(ExceptionDemo6.java:51)
	at ExceptionDemo6.f2(ExceptionDemo6.java:60)
	... 2 more

運行過程:

通過運行結果,我們看到,異常發生的整個過程都打印到了屏幕上,這就是一個異常鏈。

8. 小結

通過本小節的學習,我們知道了異常就是程序上的錯誤,良好的異常處理可以提高代碼的健壯性。Java 語言中所有錯誤(Error)和異常(Exception)的父類都是 ThrowableErrorExceptionThrowable 的直接子類,我們通常說的異常處理實際上就是處理 Exception 及其子類,異常又分為檢查型異常非檢查型異常。通過拋出異常和捕獲異常來實現異常處理。我們亦可以通過繼承 Throwable 類或者它的子類來自定義異常類。通過構造方法獲取之前異常的信息可以實現異常鏈。