超詳細講解Java異常

一、Java異常架構與異常關鍵字

Java異常簡介

Java 異常是 Java 提供的一種識別及響應錯誤的一致性機制。

Java 異常機制可以使程序中異常處理代碼和正常業務代碼分離,保證程序代碼更加優雅,並提高程序健壯性。

在有效使用異常的情況下,異常能清晰的回答 what,where,why 這3個問題:

  • 異常類型回答瞭 “什麼” 被拋出
  • 異常堆棧跟蹤回答瞭 “在哪” 拋出
  • 異常信息回答瞭 “為什麼” 會拋出

Java異常架構

1、Throwable

Throwable 是 Java 語言中所有錯誤與異常的超類。

Throwable 包含兩個子類:Error(錯誤)和 Exception(異常),它們通常用於指示發生瞭異常情況。

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

2、Error(錯誤)

定義:Error 類及其子類。程序中無法處理的錯誤,表示運行應用程序中出現瞭嚴重的錯誤。

特點:此類錯誤一般表示代碼運行時 JVM 出現問題。

通常有 Virtual MachineError(虛擬機運行錯誤)、NoClassDefFoundError(類定義錯誤)等。

比如 OutOfMemoryError:內存不足錯誤;StackOverflowError:棧溢出錯誤。此類錯誤發生時,JVM 將終止線程。

這些錯誤是不受檢異常,非代碼性錯誤。因此,當此類錯誤發生時,應用程序不應該去處理此類錯誤。

按照 Java 慣例,我們是不應該實現任何新的 Error 子類的!

3、Exception(異常)

程序本身可以捕獲並且可以處理的異常。Exception 這種異常又分為兩類:運行時異常和編譯時異常。

運行時異常

定義:RuntimeException 類及其子類,表示 JVM 在運行期間可能出現的異常。

特點:Java 編譯器不會檢查它。也就是說,當程序中可能出現這類異常時,倘若既 “沒有通過throws聲明拋出它”,也 “沒有用try-catch語句捕獲它”,還是會編譯通過。

比如 NullPointerException 空指針異常、ArrayIndexOutBoundException 數組下標越界異常、ClassCastException 類型轉換異常、ArithmeticExecption 算術異常。

此類異常屬於不受檢異常,一般是由程序邏輯錯誤引起的,在程序中可以選擇捕獲處理,也可以不處理。

雖然 Java 編譯器不會檢查運行時異常,但是我們也可以通過 throws 進行聲明拋出,也可以通過 try-catch 對它進行捕獲處理。

如果產生運行時異常,則需要通過修改代碼來進行避免。例如,若會發生除數為零的情況,則需要通過代碼避免該情況的發生!

RuntimeException 異常會由 Java 虛擬機自動拋出並自動捕獲,就算我們沒寫異常捕獲語句運行時也會拋出錯誤!

此類異常的出現絕大數情況是代碼本身有問題,應該從邏輯上去解決並改進代碼。

編譯時異常

定義:Exception 中除 RuntimeException 及其子類之外的異常。

特點:Java 編譯器會檢查它。如果程序中出現此類異常,比如 ClassNotFoundException(沒有找到指定的類異常),IOException(IO流異常),要麼通過 throws 進行聲明拋出,要麼通過 try-catch 進行捕獲處理,否則不能通過編譯。

在程序中,通常不會自定義該類異常,而是直接使用系統提供的異常類。

該異常我們必須手動在代碼裡添加捕獲語句來處理該異常

4、受檢異常與非受檢異常

Java 的所有異常可以分為受檢異常(checked exception)和非受檢異常(uncheckedexception)。

受檢異常

編譯器要求必須處理的異常。

正確的程序在運行過程中,經常容易出現的、符合預期的異常情況。一旦發生此類異常,就必須采用某種方式進行處理。

除 RuntimeException 及其子類外,其他的 Exception 異常都屬於受檢異常,編譯器會檢查此類異常。

也就是說當編譯器檢查到應用中的某處可能會此類異常時,將會提示你處理本異常,

要麼使用try-catch捕獲,要麼使用方法簽名中用 throws 關鍵字拋出,否則編譯不通過。

非受檢異常

編譯器不會進行檢查並且不要求必須處理的異常。

也就說當程序中出現此類異常時,即使我們沒有 try-catch 捕獲它,也沒有使用throws拋出該異常,編譯也會正常通過。

該類異常包括運行時異常(RuntimeException 及其子類)和錯誤(Error)

Java異常關鍵字

  • try:用於監聽。

將要被監聽的代碼(可能拋出異常的代碼)放在 try 語句塊之內,當 try 語句塊內發生異常時,異常就被拋出。

  • catch:用於捕獲異常。

catch 用來捕獲 try 語句塊中發生的異常。

  • finally:finally 語句塊總是會被執行。

它主要用於回收在 try 塊裡打開的物力資源(如數據庫連接、網絡連接和磁盤文件)。

隻有 finally 塊,執行完成之後,才會回來執行 try 或者 catch 塊中的 return 或者 throw 語句,

如果 finally 中使用瞭 return 或者 throw 等終止方法的語句,則就不會跳回執行,直接停止。

  • throw:用於拋出異常。
  • throws:用在方法簽名中,用於聲明該方法可能拋出的異常。

二、Java異常處理

Java 通過面向對象的方法進行異常處理,一旦方法拋出異常,系統自動根據該異常對象尋找合適異常處理器(Exception Handler)來處理該異常,把各種不同的異常進行分類,並提供瞭良好的接口。

在 Java 中,每個異常都是一個對象,它是 Throwable 類或其子類的實例。

當一個方法出現異常後便拋出一個異常對象,該對象中包含有異常信息,調用這個對象的方法可以捕獲到這個異常並可以對其進行
處理。

Java 的異常處理是通過 5 個關鍵詞來實現的:try、catch、throw、throws 和 finally。

在Java應用中,異常的處理機制分為聲明異常拋出異常捕獲異常

聲明異常

通常,應該捕獲那些知道如何處理的異常,將不知道如何處理的異常繼續傳遞下去。

傳遞異常可以在方法簽名處使用 throws 關鍵字聲明可能會拋出的異常。

註意:

  • 非檢查異常(Error、RuntimeException 或它們的子類)不可使用 throws 關鍵字來聲明要拋出的異常。
  • 一個方法出現編譯時異常,就需要 try-catch/ throws 處理,否則會導致編譯錯誤。

拋出異常

如果你覺得解決不瞭某些異常問題,且不需要調用者處理,那麼你可以拋出異常。

throw 關鍵字作用是在方法內部拋出一個 Throwable 類型的異常。

任何Java代碼都可以通過 throw 語句拋出異常。

捕獲異常

程序通常在運行之前不報錯,但是運行後可能會出現某些未知的錯誤,但是還不想直接拋出到上一級,

那麼就需要通過 try…catch… 的形式進行異常捕獲,之後根據不同的異常情況來進行相應的處理。

如何選擇異常類型

可以根據下圖來選擇是捕獲異常,聲明異常還是拋出異常

常見異常處理方式

1、直接拋出異常

通常,應該捕獲那些知道如何處理的異常,將不知道如何處理的異常繼續傳遞下去。

傳遞異常可以在方法簽名處使用 throws 關鍵字聲明可能會拋出的異常。

public static void readFile(String filePath) throws IOException {
    BufferedReader reader = new BufferedReader(new FileReader(filePath));
    String res;

    while ( (res = reader.readLine()) != null ) {
        System.out.println(res);
    }
    reader.close();
}

2、封裝異常再拋出

有時我們會從 catch 中拋出一個異常,目的是為瞭改變異常的類型。多用於在多系統集成時,

當某個子系統故障,異常類型可能有多種,可以用統一的異常類型向外暴露,不需暴露太多內部異常細節。

自定義一個異常 MyException

public class MyException extends Exception {

    public MyException() {
        super();
    }

    public MyException(String message) {
        super(message);
    }
}

使用自定義異常封裝異常

public static void readFile(String filePath) throws MyException {
    BufferedReader reader = null;
    String res;
    try {
        reader = new BufferedReader(new FileReader(filePath));
        while ( (res = reader.readLine()) != null ) {
            System.out.println(res);
        }
    } catch (IOException e) {
        MyException myException = new MyException("讀取文件失敗!");
        myException.initCause(e);
        throw myException;
    }

    try {
        reader.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

3、捕獲異常

在一個 try-catch 語句塊中可以捕獲多個異常類型,並對不同類型的異常做出不同的處理。

public static void readFile(String filePath) {
    try {
        // do
    } catch (FileNotFoundException e) {
        // handle FileNotFoundException
    } catch (IOException e) {
        // handle IOException
    }
}

同一個 catch 也可以捕獲多種類型異常,用 | 隔開

public static void readFile(String filePath) {
    try {
        // do
    } catch (FileNotFoundException | UnknownHostException e) {
        // handle FileNotFoundException or UnknownHostException
    } catch (IOException e) {
        // handle IOException
    }
}

4、自定義異常

習慣上,定義一個異常類應包含兩個構造函數,一個無參構造函數和一個帶有詳細描述信息的構造函數,

Throwable 的 toString 方法會打印這些詳細信息,調試時很有用。

public class MyException extends Exception {

    public MyException() {
        super();
    }

    public MyException(String message) {
        super(message);
    }
}

5、try-catch-finally

當方法中發生異常,異常處之後的代碼不會再執行,如果之前獲取瞭一些本地資源需要釋放,

則需要在方法正常結束時和 catch 語句中都調用釋放本地資源的代碼,顯得代碼比較繁瑣,

finally 語句可以解決這個問題。

public static void readFile(String filePath) throws MyException {
    BufferedReader reader = null;
    String res;
    try {
        reader = new BufferedReader(new FileReader(filePath));
        while ( (res = reader.readLine()) != null ) {
            System.out.println(res);
        }
    } catch (IOException e) {
        System.out.println("catch 代碼塊");
        MyException myException = new MyException("讀取文件失敗!");
        myException.initCause(e);
        throw myException;
    } finally {
        System.out.println("finally 代碼塊");
        if (null != reader) {
            try {
                reader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

調用該方法時,讀取文件時若發生異常,代碼會進入 catch 代碼塊,之後進入 finally 代碼塊;

若讀取文件時未發生異常,則會跳過 catch 代碼塊直接進入 finally 代碼塊。

所以無論代碼中是否發生異常,fianlly 中的代碼都會執行。

若 catch 代碼塊中包含 return 語句,finally 中的代碼還會執行嗎?將以上代碼中的 catch 子句修改如下:

catch (IOException e) {
    System.out.println("catch 代碼塊");
    return ;
} 

調用 readFile 方法,觀察當 catch 子句中調用 return 語句時,finally 子句是否執行

catch 代碼塊
finally 代碼塊

可見,即使 catch 中包含瞭 return 語句,finally 子句依然會執行。

若 finally 中也包含 return 語句,finally 中的 return 會覆蓋前面的 return。

6、try-with-resource

上面例子中,finally 中的 close 方法也可能拋出 IOException,從而覆蓋瞭原始異常。

JAVA 7 提供瞭更優雅的方式來實現資源的自動釋放,自動釋放的資源需要是實現瞭 AutoCloseable 接口的類。

public static void tryWithResourceTest() {
    try {
        Scanner scanner = new Scanner(new FileInputStream("d:/"),"UTF-8");
    } catch (IOException e) {
        // handle IOException
    }
}

try 代碼塊退出時,會自動調用 scanner.close 方法,和把 scanner.close 方法放在 finally 代碼塊中不同的是,

若 scanner.close 拋出異常,則會被抑制,拋出的仍然為原始異常。

被抑制的異常會由 addSusppressed 方法添加到原來的異常,如果想要獲取被抑制的異常列表,

可以調用 getSuppressed 方法來獲取。

三、Java異常常見面試題

1、Error 和 Exception 區別是什麼?

Error 類型的錯誤通常為虛擬機相關錯誤,如系統崩潰,內存不足,堆棧溢出等,

編譯器不會對這類錯誤進行檢測,JAVA 應用程序也不應對這類錯誤進行捕獲,

一旦這類錯誤發生,通常應用程序會被終止,僅靠應用程序本身無法恢復;

Exception 類的錯誤是可以在應用程序中進行捕獲並處理的,通常遇到這種錯誤,

應對其進行處理,使應用程序可以繼續正常運行。

2、運行時異常和一般異常(受檢異常)區別是什麼?

運行時異常包括 RuntimeException 類及其子類,表示 JVM 在運行期間可能出現的異常。

Java 編譯器不會檢查運行時異常。受檢異常是 Exception 中除 RuntimeException 及其子類之外的異常。

Java 編譯器會檢查受檢異常。

RuntimeException異常和受檢異常之間的區別:是否強制要求調用者必須處理此異常,

如果強制要求調用者必須進行處理,那麼就使用受檢異常,否則就選擇非受檢異常(RuntimeException)。

一般來講,如果沒有特殊的要求,我們建議使用 RuntimeException 異常。

3、JVM 是如何處理異常的?

在一個方法中如果發生異常,這個方法會創建一個異常對象,並轉交給 JVM,

該異常對象包含異常名稱,異常描述以及異常發生時應用程序的狀態。

創建異常對象並轉交給 JVM 的過程稱為拋出異常

可能有一系列的方法調用,最終才進入拋出異常的方法,這一系列方法調用的有序列表叫做調用棧

JVM 會順著調用棧去查找看是否有可以處理異常的代碼,如果有,則調用異常處理代碼。

當 JVM 發現可以處理異常的代碼時,會把發生的異常傳遞給它。

如果 JVM 沒有找到可以處理該異常的代碼塊,JVM 就會將該異常轉交給默認的異常處理器,

默認異常處理器為 JVM 的一部分,默認異常處理器會打印出異常信息並終止應用程序。

4、throw 和 throws 的區別是什麼?

Java 中的異常處理除瞭包括捕獲異常和處理異常之外,還包括聲明異常和拋出異常,

可以通過 throws 關鍵字在方法上聲明該方法要拋出的異常,或者在方法內部通過 throw 拋出異常對象。

throws 關鍵字和 throw 關鍵字在使用上的幾點區別如下:

  • throw 關鍵字用在方法內部,隻能用於拋出一種異常,用來拋出方法或代碼塊中的異常,

受查異常和非受查異常都可以被拋出;

  • throws 關鍵字用在方法聲明上,可以拋出多個異常,用來標識該方法可能拋出的異常列表。

一個方法用 throws 標識瞭可能拋出的異常列表,調用該方法的方法中必須包含可處理異常的代碼,

否則也要在方法簽名中用 throws 關鍵字聲明相應的異常。

5、final、finally、finalize 有什麼區別?

  • final 可以修飾類、變量、方法,修飾類表示該類不能被繼承、修飾方法表示該方法不能被重寫、修飾變量

表示該變量是一個常量不能被重新賦值。

  • finally一般作用在 try-catch 代碼塊中,在處理異常的時候,通常我們將一定要執行的代碼方法 finally 代碼

塊中,表示不管是否出現異常,該代碼塊都會執行,一般用來存放一些關閉資源的代碼。

  • finalize 是一個方法,屬於 Object 類的一個方法,而 Object 類是所有類的父類,Java 中允許使用 finalize()

方法在垃圾收集器將對象從內存中清除出去之前做必要的清理工作。

6、NoClassDefFoundError 和 ClassNotFoundException 區別?

NoClassDefFoundError 是一個 Error 類型的異常,是由 JVM 引起的,不應該嘗試捕獲這個異常。

引起該異常的原因是 JVM 或 ClassLoader 嘗試加載某類時在內存中找不到該類的定義,

該動作發生在運行期間,即編譯時該類存在,但是在運行時卻找不到瞭,可能是變異後被刪除瞭等原因導致;

ClassNotFoundException 是一個受查異常,需要顯式地使用 try-catch 對其進行捕獲和處理,

或在方法簽名中用 throws 關鍵字進行聲明。

當使用 Class.forName,ClassLoader.loadClass 或 ClassLoader.findSystemClass 動態加載類到內存的時候,

通過傳入的類路徑參數沒有找到該類,就會拋出該異常;

另一種拋出該異常的可能原因是某個類已經由一個類加載器加載至內存中,另一個加載器又嘗試去加載它。

7、try-catch-finally 中哪個部分可以省略?

catch 可以省略。

原因

更為嚴格的說法其實是:try 隻適合處理運行時異常,try+catch 適合處理運行時異常+普通異常。

也就是說,如果你隻用try去處理普通異常卻不加以catch處理,編譯是通不過的,

因為編譯器硬性規定,普通異常如果選擇捕獲,則必須用 catch 顯示聲明以便進一步處理。

而運行時異常在編譯時沒有如此規定,所以 catch 可以省略,你加上 catch 編譯器也覺得無可厚非。

理論上,編譯器看任何代碼都不順眼,都覺得可能有潛在的問題,所以你即使對所有代碼加上try,

代碼在運行期時也隻不過是在正常運行的基礎上加一層皮。

但是你一旦對一段代碼加上 try,就等於顯示地承諾編譯器,對這段代碼可能拋出的異常進行捕獲,而非向上拋出處理。

如果是普通異常,編譯器要求必須用 catch 捕獲以便進一步處理;

如果運行時異常,捕獲然後丟棄並且+ finally 掃尾處理,或者加上 catch 捕獲以便進一步處理。

至於加上 finally,則是在不管有沒捕獲異常,都要進行的 “掃尾” 處理。

8、try-catch-finally 中,如果 catch 中 return 瞭,finally 還會執行嗎?

會執行,在 return 前執行。

註意:在 finally 中改變返回值的做法是不好的,因為如果存在 finally 代碼塊,try中的 return 語句不會立馬返回調用者,

而是記錄下返回值待 finally 代碼塊執行完畢之後再向調用者返回其值,然後如果在 finally 中修改瞭返回值,就會返回修改後的值。

顯然,在 finally 中返回或者修改返回值會對程序造成很大的困擾,C# 中直接用編譯錯誤的方式來阻止程序員幹這種事情,

Java 中也可以通過提升編譯器的語法檢查級別來產生警告或錯誤。

例子1

public static int getInt() {
    int a = 10;
    try {
        System.out.println(a / 0);
        a = 20;
    } catch (ArithmeticException e) {
        a = 30;
        // 程序執行到這裡時,不是 return a,而是 return 30,這個返回路徑已經形成
        // 但是後面還有 finally,所以繼續執行 finally 內容,a = 40
        // 再次回到這裡,繼續 return 30,形成返回路徑後,這裡的 a 就不是變量瞭,
        // 而是常量 30,所以結果為 30
        return a;
    } finally {
        a = 40;
    }
    return a;
}

執行結果:30

例子2

public static int getInt() {
    int a = 10;
    try {
        System.out.println(a / 0);
        a = 20;
    } catch (ArithmeticException e) {
        a = 30;
        return a;
    } finally {
        a = 40;
        // 如果 finally 中 return,這樣就重新又形成瞭一條返回路徑,
        // 由於隻能通過 1 個 return 返回,所以這裡直接返回 40
        return a;
    }
}

執行結果:40

9、類 ExampleA 繼承 Exception,類 ExampleB 繼承ExampleA。

有如下代碼片斷:

try {
    throw new ExampleB("B");
} catch (ExampleA e) {
    System.out.println("ExampleA");
} catch (Exception e) {
    System.out.println("Exception");
}

請問執行結果是什麼?

輸出:ExampleA

說明:根據裡氏代換原則(能使用父類型的地方一定能使用子類型),

抓取 ExampleA 類型異常的 catch 塊能夠抓住 try 塊中拋出的 ExampleB 類型的異常。

10、說出下面代碼的運行結果

class AnnoyanceException extends Exception {
}

class SneezeException extends AnnoyanceException {
}

public class Test04 {
    public static void main(String[] args) throws Exception {
        try {
            try {
                throw new SneezeException();
            } catch (AnnoyanceException a) {
                System.out.println("Caught Annoyance");
                throw a;
            }
        } catch (SneezeException s) {
            System.out.println("Caught Sneeze");
            return;
        } finally {
            System.out.println("Hello World");
        }
    }
}

結果:

Caught Annoyance
Caught Sneeze
Hello World

11、常見的 RuntimeException 有哪些?

  • ClassCastException(類轉換異常)
  • IndexOutOfBoundsException(數組越界)
  • NullPointerException(空指針)
  • ArrayStoreException(數據存儲異常,操作數組時類型不一致)
  • 還有IO操作的 BufferOverflowException 異常

12、Java常見異常有哪些

  • java.lang.IllegalAccessError:違法訪問錯誤。當一個應用試圖訪問、修改某個類的域(Field)或者調用其方法,但是又違反域或方法的可見性聲明,則拋出該異常。
  • java.lang.InstantiationError:實例化錯誤。當一個應用試圖通過 Java 的 new 操作符構造一個抽象類或者接口時拋出該異常。
  • java.lang.OutOfMemoryError:內存不足錯誤。當可用內存不足以讓 Java 虛擬機分配給一個對象時拋出該錯誤。
  • java.lang.StackOverflowError:堆棧溢出錯誤。當一個應用遞歸調用的層次太深而導致堆棧溢出或者陷入死循環時拋出該錯誤。
  • java.lang.ClassCastException:類造型異常。假設有類 A 和 B(A 不是 B 的父類或子類),O 是 A 的實例,那麼當強制將 O 構造為類B的實例時拋出該異常。該異常經常被稱為強制類型轉換異常。
  • java.lang.ClassNotFoundException:找不到類異常。當應用試圖根據字符串形式的類名構造類,而在遍歷 CLASSPAH 之後找不到對應名稱的 class 文件時,拋出該異常。
  • java.lang.ArithmeticException:算術條件異常。譬如:整數除零等。
  • java.lang.ArrayIndexOutOfBoundsException:數組索引越界異常。當對數組的索引值為負數或大於等於數組大小時拋出。
  • java.lang.IndexOutOfBoundsException:索引越界異常。當訪問某個序列的索引值小於0或大於等於序列大小時,拋出該異常。
  • java.lang.InstantiationException:實例化異常。當試圖通過 newInstance() 方法創建某個類的實例,而該類是一個抽象類或接口時,拋出該異常。
  • java.lang.NoSuchFieldException:屬性不存在異常。當訪問某個類的不存在的屬性時拋出該異常。
  • java.lang.NoSuchMethodException:方法不存在異常。當訪問某個類的不存在的方法時拋出該異常。
  • java.lang.NullPointerException:空指針異常。當應用試圖在要求使用對象的地方使用瞭 null 時,拋出該異常。譬如:調用 null 對象的實例方法、訪問 null 對象的屬性、計算 null 對象的長度、使用 throw 語句拋出 null 等等。
  • java.lang.NumberFormatException:數字格式異常。當試圖將一個 String 轉換為指定的數字類型,而該字符串確不滿足數字類型要求的格式時,拋出該異常。
  • java.lang.StringIndexOutOfBoundsException:字符串索引越界異常。當使用索引值訪問某個字符串中的字符,而該索引值小於0或大於等於序列大小時,拋出該異常。

13、Java 異常中 Throwable、Error、Exception、RuntimeException 的區別

  • Throwable 類是 Java 語言中所有錯誤或異常的超類。它的兩個子類是 Error 和 Exception;
  • Error 是 Throwable 的子類,用於指示合理的應用程序不應該試圖捕獲的嚴重問題,如內存溢出、虛擬機錯誤、棧溢出等。

這類錯誤一般與硬件有關,與程序本身無關,通常由系統進行處理,程序本身無法捕獲和處理。

OutOfMemoryError 內存溢出

java.lang.StackOverflowError 堆棧溢出錯誤,當一個應用遞歸調用的層次太深而導致堆棧溢出時拋出該錯誤

  • Exception 類及其子類是 Throwable 的一種形式,合理的應用程序想要捕獲的條件,有些異常在編寫程序時無法預料的,

如中斷異常、非法存取異常等。為瞭保證程序的健壯性,Java 要求必須對這些可能出現的異常進行捕獲,並對其進行處理

  • RuntimeException 類是 Exception 類的子類(可能在Java虛擬機正常運行期間拋出的異常的超類);
  • IOExeption類是Exception類的子類。

四、Java異常處理最佳實踐

在 Java 中處理異常並不是一個簡單的事情。不僅僅初學者很難理解,即使一些有經驗的開發者,

也需要花費很多時間來思考如何處理異常,包括需要處理哪些異常,怎樣處理等等。

這也是絕大多數開發團隊都會制定一些規則來規范進行異常處理的原因。而團隊之間的這些規范往往是截然不同的。

本文給出幾個被很多團隊使用的異常處理最佳實踐。

1、在 finally 塊中清理資源或者使用 try-with-resource 語句

當使用類似 InputStream 這種需要使用後關閉的資源時,一個常見的錯誤就是在 try 塊的最後關閉資源。

public static void readFile(String filePath) {
    BufferedReader reader = null;
    String res;
    try {
        reader = new BufferedReader(new FileReader(filePath));
        while ( (res = reader.readLine()) != null ) {
            System.out.println(res);
        }
        reader.close(); // 錯誤!
    } catch (IOException e) {
        e.printStackTrace();
    }
}

問題就是,隻有沒有異常拋出的時候,這段代碼才可以正常工作。try 代碼塊內代碼會正常執行,

並且資源可以正常關閉。但是,使用 try 代碼塊是有原因的,一般調用一個或多個可能拋出異常的方法,

而且,你自己也可能會拋出一個異常,這意味著代碼可能不會執行到 try 代碼塊的最後部分。

結果就是,並沒有關閉資源。所以,應該把清理工作的代碼放到 finally 裡去,或者使用 try-with-resource 特性。

(1)使用 finally 代碼塊

與前面幾行 try 代碼塊不同,finally 代碼塊總是會被執行。

不管 try 代碼塊成功執行之後還是在 catch 代碼塊中處理完異常後都會執行。

因此,可以確保這裡清理瞭所有打開的資源。

public static void readFile(String filePath) {
    BufferedReader reader = null;
    String res;
    try {
        reader = new BufferedReader(new FileReader(filePath));
        while ( (res = reader.readLine()) != null ) {
            System.out.println(res);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (null != reader) { // 正確
            try {   
                reader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

(2)Java 7 的 try-with-resource 語法

如果你的資源實現瞭 AutoCloseable 接口,你可以使用這個語法。大多數的 Java 標準資源都繼承瞭這個接口。

當你在 try 子句中打開資源,資源會在 try 代碼塊執行後或異常處理後自動關閉。

public static void readFile(String filePath) {
    String res;
    try {
        BufferedReader reader = new BufferedReader(new FileReader(filePath));
        while ( (res = reader.readLine()) != null ) {
            System.out.println(res);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

2、優先明確的異常

你拋出的異常越明確越好,永遠記住,你的同事或者幾個月之後的你,將會調用你的方法並且處理異常。

因此需要保證提供給他們盡可能多的信息。這樣你的 API 更容易被理解。

你的方法的調用者能夠更好的處理異常並且避免額外的檢查。因此,總是嘗試尋找最適合你的異常事件的類。

例如,拋出一個 NumberFormatException ,來替換一個 IllegalArgumentException 。避免拋出一個不明確的異常。

public void doNotDoThis() throws Exception {
    
}
public void doThis() throws NumberFormatException {
    
}

3、對異常進行文檔說明

當在方法上聲明拋出異常時,也需要進行文檔說明。目的是為瞭給調用者提供盡可能多的信息,

從而可以更好地避免或處理異常。在 Javadoc 添加 @throws 聲明,並且描述拋出異常的場景。

4、使用描述性消息拋出異常

在拋出異常時,需要盡可能精確地描述問題和相關信息,這樣無論是打印到日志中還是在監控工具中,

都能夠更容易被人閱讀,從而可以更好地定位具體錯誤信息、錯誤的嚴重程度等。

但這裡並不是說要對錯誤信息長篇大論,因為本來 Exception 的類名就能夠反映錯誤的原因,因此隻需要用一到兩句話描述即可。

如果拋出一個特定的異常,它的類名很可能已經描述瞭這種錯誤。所以你不需要提供很多額外的信息。

一個很好的例子是 NumberFormatException 。當你以錯誤的格式提供 String 時,它將被 java.lang.Long 類的構造函數拋出。

try {
    new Long("xxx");
} catch (NumberFormatException e) {
    e.printStackTrace();
}

拋出

java.lang.NumberFormatException: For input string: "xxx"
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Long.parseLong(Long.java:589)
	at java.lang.Long.<init>(Long.java:965)

5、優先捕獲最具體的異常

大多數 IDE 都可以幫助你實現這個最佳實踐。當你嘗試首先捕獲較不具體的異常時,它們會報告無法訪問的代碼塊。

但問題在於,隻有匹配異常的第一個 catch 塊會被執行。

因此,如果首先捕獲 IllegalArgumentException,則永遠不會到達應該處理更具體的 NumberFormatException 的 catch 塊,

因為它是 IllegalArgumentException 的子類。

總是優先捕獲最具體的異常類,並將不太具體的 catch 塊添加到列表的末尾。

public static void catchMostSpecificExceptionFirst() {
    try {
        // do
    } catch (NumberFormatException e) {
        // handle NumberFormatException
    } catch (IllegalArgumentException e) {
        // handle IllegalArgumentException
    }
}

6、不要捕獲 Throwable 類

Throwable 是所有異常和錯誤的超類。你可以在 catch 子句中使用它,但是你永遠不應該這樣做!

如果在 catch 子句中使用 Throwable ,它不僅會捕獲所有異常,也將捕獲所有的錯誤。

JVM 拋出錯誤,指出不應該由應用程序處理的嚴重問題。

典型的例子是 OutOfMemoryError 或者 StackOverflowError 。

兩者都是由應用程序控制之外的情況引起的,無法處理。所以,最好不要捕獲 Throwable ,

除非你確定自己處於一種特殊的情況下能夠處理錯誤。

7、不要忽略異常

很多時候,開發者很有自信不會拋出異常,因此寫瞭一個catch塊,但是沒有做任何處理或者記錄日志。

public void doNotIgnoreExceptions() { 
    try{ 
        // do 
    } catch (NumberFormatException e){ 
        // this will neverhappen
    }
}

但現實是經常會出現無法預料的異常,或者無法確定這裡的代碼未來是不是會改動(刪除瞭阻止異常拋出的代碼),

而此時由於異常被捕獲,使得無法拿到足夠的錯誤信息來定位問題。合理的做法是至少要記錄異常的信息。

public void logAnException() { 
    try{ 
        // do 
    } catch (NumberFormatException e){ 
        log.error("This should never happen: "+ e)
    }
}

8、不要記錄並拋出異常

這可能是最常被忽略的最佳實踐。可以發現很多代碼甚至類庫中都會有捕獲異常、記錄日志並再次拋出的邏輯。

try {
    new Long("xxx");
} catch (NumberFormatException e) {
    log.error(e);
    throw e;
}

這個處理邏輯看著是合理的。但這經常會給同一個異常輸出多條日志。

多餘的日志也沒有附加更有用的信息。如果想要提供更加有用的信息,那麼可以將異常包裝為自定義異常。

public void wrapException(String input) throws MyException {
    try { 
        // do 
    } catch(NumberFormatException e) { 
        throw new MyException("A message that describesthe error.", e);
    }
}

因此,僅僅當想要處理異常時才去捕獲,否則隻需要在方法簽名中聲明讓調用者去處理。

9、包裝異常時不要拋棄原始的異常

捕獲標準異常並包裝為自定義異常是一個很常見的做法。這樣可以添加更為具體的異常信息並能夠做針對的異常處理。

在你這樣做時,請確保將原始異常設置為原因(註:參考下方代碼 NumberFormatException e 中的原始異常 e )。

Exception 類提供瞭特殊的構造函數方法,它接受一個 Throwable 作為參數。

否則,你將會丟失堆棧跟蹤和原始異常的消息,這將會使分析導致異常的異常事件變得困難。

public void wrapException(String input) throws MyException {
    try { 
        // do 
    } catch(NumberFormatException e) { 
        throw new MyException("A message that describesthe error.", e);
        // 等價於 
        // MyException myException = new MyException("A message that describesthe error.");
        // myException.initCause(e);
        // throw myException;
    }
}

10、不要使用異常控制程序的流程

不應該使用異常控制應用的執行流程,例如,本應該使用if語句進行條件判斷的情況下,

你卻使用異常處理,這是非常不好的習慣,會嚴重影響應用的性能。

11、使用標準異常

如果使用內建的異常可以解決問題,就不要定義自己的異常。

Java API 提供瞭上百種針對不同情況的異常類型,在開發中首先盡可能使用 Java API 提供的異常,

如果標準的異常不能滿足你的要求,這時候創建自己的定制異常。盡可能得使用標準異常有利於新加入的開發者看懂項目代碼。

12、異常會影響性能

異常處理的性能成本非常高,每個 Java 程序員在開發時都應牢記這句話。

創建一個異常非常慢,拋出一個異常又會消耗1~5ms,

當一個異常在應用的多個層級之間傳遞時,會拖累整個應用的性能。

  • 僅在異常情況下使用異常;
  • 在可恢復的異常情況下使用異常;

盡管使用異常有利於 Java 開發,但是在應用中最好不要捕獲太多的調用棧,

因為在很多情況下都不需要打印調用棧就知道哪裡出錯瞭。

因此,異常消息應該提供恰到好處的信息。

13、總結

綜上所述,當你拋出或捕獲異常的時候,有很多不同的情況需要考慮,

而且大部分事情都是為瞭改善代碼的可讀性或者 API 的可用性。

異常不僅僅是一個錯誤控制機制,也是一個通信媒介。

因此,為瞭和同事更好的合作,一個團隊必須要制定出一個最佳實踐和規則,

隻有這樣,團隊成員才能理解這些通用概念,同時在工作中使用它。

五、異常處理-阿裡巴巴Java開發手冊

到此這篇關於超詳細講解Java異常的文章就介紹到這瞭,更多相關Java異常內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: