Java核心知識體系3:異常機制詳解

2022-08-04 21:02:11

1 什麼是異常

異常是指程式在執行過程中發生的,由於外部問題導致的執行異常事件,如:檔案找不到、網路連線失敗、空指標、非法引數等。
異常是一個事件,它發生在程式執行期間,且中斷程式的執行。
Java 是一種物件導向的程式語言,它的異常都是物件,是Throwable子類的範例,當程式中存在錯誤條件時,且條件生成時,錯誤就會引發異常。

2 異常的分類

要了解異常的分類,我們先看看Java異常類的繼承結構圖:

2.1 Throwable

Throwable 是 Java 語言中所有錯誤與異常的頂層父類別,其他類都繼承於該類。 Throwable 包含兩個子類:Error(錯誤)和 Exception(異常),它們通常用於指示發生了異常情況。 Throwable 包含了其執行緒建立時執行緒執行堆疊的快照,它提供了 printStackTrace() 等介面用於獲取堆疊跟蹤資料等資訊。

2.2 Error(錯誤)

Error 類及其子類:程式中無法處理的錯誤,表示執行應用程式中出現了嚴重的錯誤。通常情況為下應用程式 "不應該試圖捕獲的嚴重問題"
此類錯誤一般表示程式碼執行時 JVM 出現問題。通常有 Virtual MachineError(虛擬機器器執行錯誤)、NoClassDefFoundError(類定義錯誤)等。比如 OutOfMemoryError:記憶體不足錯誤;StackOverflowError:棧溢位錯誤。這些錯誤是不可查的,因為它們在應用程式的控制和處理能力之 外,而且絕大多數是程式執行時不允許出現的狀況。在 Java中,錯誤通過Error的子類描述。

2.3 Exception(異常)

Exception以及它的子類,代表程式執行時傳送的各種不期望發生的事件。可以被Java例外處理機制使用,是例外處理的核心。Exception 這種異常又分為兩類:執行時異常和編譯時異常。

2.3.1 執行時異常

都是RuntimeException類及其子類異常,如NullPointerException(空指標異常)、IndexOutOfBoundsException(下標越界異常)等,這些異常是不檢查異常,程式中可以選擇捕獲處理,也可以不處理。這些異常一般是由程式邏輯錯誤引起的,程式應該從邏輯角度儘可能避免這類異常的發生。 執行時異常的特點是Java編譯器不會檢查它,也就是說,當程式中可能出現這類異常,即使沒有用try-catch語句捕獲它,也沒有用throws子句宣告丟擲它,也會編譯通過。

2.3.2 非執行時異常 (編譯異常)

是RuntimeException以外的異常,型別上都屬於Exception類及其子類。從程式語法角度講是必須進行處理的異常,如果不處理,程式就不能編譯通過。如IOException、SQLException等以及使用者自定義的Exception異常,一般情況下不自定義檢查異常。

2.3.3 檢查性異常(checked exception)

正確的程式在執行中,很容易出現的、情理可容的異常狀況。可查異常雖然是異常狀況,但在一定程度上它的發生是可以預計的,而且一旦發生這種異常狀況,就必須採取某種方式進行處理。
除了Error 和 RuntimeException的其它異常。Java語言強制要求程式設計師為這樣的異常做預備處理工作(使用try…catch…finally或者throws)。在方法中要麼用try-catch語句捕獲它並處理,要麼用throws子句宣告丟擲它,否則編譯不會通過。類似如SQLException,IOException,ClassNotFoundException 等。
常見的檢查性異常如下:

異常 描述
Classnotfoundexception 當應用程式試圖加數一個,通過名字查詢時超發現沒有該的定義時,丟擲該異常
Clonenotsupportedexcept 當去克一個物件時,發現該物件沒有實現Cloneable介面時,丟擲該異常
lllegalaccessexception 當應用程式芸試通過反射的方式來訪間類、成員變數或呼叫方法時,卻無法存取這些類、成員變數或方法的定義時,丟擲該異常
Instantiationexception 當試圖使用 Class:類中的 newinstance方法創建一個類的範例,而制定的類物件因為是一個介面或是一個抽象類而無法範例化時,丟擲該異常
Interruptedexception 個執行緒被另一個執行緒中斷時,丟擲該異常
NosuchFieldexception 當攏不到指定的變數欄位時,丟擲該異常
NosuchMethodexception 當我不到指定的類方法時,丟擲該異常

2.3.4 非檢查性異常(checked exception)

包括執行時異常(RuntimeException與其子類)和錯誤(Error)及其子類。
Java語言在編譯時,不會提示和發現這樣的異常,不要求在程式中處理這些異常。所以我們可以在程式中編寫程式碼來處理(使用try…catch…finally)這樣的異常,也可以不做任何處理。
但是這種錯誤或異常,一般來說是程式邏輯錯誤導致的異常,所以我們應該修正程式碼,而不是通過例外處理器處理。
常見的非檢查性異常如下:

異常 描述
Arithmeticexception 當出現異常的運算條件時,丟擲異常。例如,一個整數「除以零」時,丟擲此美的一個範例
Arrayindexoutofboundsexcep 用非法索引存取陣列時跑出的異常。如果索引為負或大於等於陣列大小,則該索引為非法索引異常描述
Arraystoreexception 試圖將錯誤型別的物件儲存到一個物件陣列時,丟擲的異常
Classcastexception 試圖將物件強制轉換為不是同一個型別或其子類的範例時,丟擲的異常
Illegalargumentexception 當向一個方法傳遞非法或不正確的引數時,丟擲該異常
IllegalmonitorstateException 當某一執行緒已經試圖等待物件的監視器,或者通知其他正在等待該物件監視器的執行緒,而該執行緒本身沒有獲得指定監視器時丟擲該異常
Illegalstateexception 在非法或不適當的時間呼叫方法時產生的訊號。或者說Java環境或應用程式沒有處於請求操作所要求的適當狀態下
IllegalthreadstateException 執行緒沒有處於請求操作所要求的適當狀態時,丟擲該異常。
Indexoutofboundsexception 當某種排序的索引超出範圍時丟擲的異常,例如,一個陣列,字串或一個向量的排序等
Negativearraysizeexception 如果應用程式試圖建立大小為負的陣列時,丟擲該異常
Nullpointerexception 當應用程式在需要操作物件的時候而獲得的物件範例是nu時丟擲該異常
Numberformatexception 當應用程式試圖將字串轉換成一種數值型別,但該字串不能轉換為適當格式時,丟擲該異常。
SecurityException 由安全管理器丟擲的異常,指示存在安全侵犯。
StringindexoutofboundsExcept 此異常由 String方法丟擲,說明索引為負或者超出了字串的大小

3 異常基礎詳解

3.1 異常關鍵字

  1. try – 用於監聽。
    將要被監聽的程式碼(可能丟擲異常的程式碼)放在try語句塊之內,當try語句塊內發生異常時,異常就被丟擲。
  2. catch – 用於捕獲異常。
    catch用來捕獲try語句塊中發生的異常。
  3. finally – finally語句塊總是會被執行。
    它主要用於回收在try塊裡開啟的物力資源(如資料庫連線、網路連線和磁碟檔案)。只有finally塊,執行完成之後,才會回來執行try或者catch塊中的return或者throw語句,如果finally中使用了return或者throw等終止方法的語句,則就不會跳回執行,直接停止。
  4. throw – 用於丟擲異常。
  5. throws – 用在方法簽名中,用於宣告該方法可能丟擲的異常。

3.2 throws-異常的顯示宣告

在Java中,當前執行的語句必屬於某個方法,Java直譯器呼叫main方法執行開始執行程式。若方法中存在檢查異常,如果不對其捕獲,那必須在方法頭中顯式宣告該異常,以便於告知方法呼叫者此方法有異常,需要進行處理。 在方法中宣告一個異常,方法頭中使用關鍵字throws,後面接上要宣告的異常。若宣告多個異常,則使用逗號分割。如下所示:

public static void yourMethod() throws Exception{
    //todo  業務邏輯
}

注意:若是父類別的方法沒有宣告異常,則子類繼承方法後,也不能宣告異常。

通常,應該捕獲那些知道如何處理的異常,將不知道如何處理的異常繼續傳遞下去。傳遞異常可以在方法簽名處使用 throws 關鍵字宣告可能會丟擲的異常。

private static void readFile(String filePath) throws IOException {
    File file = new File(filePath);
    String result;
    BufferedReader reader = new BufferedReader(new FileReader(file));
    while((result = reader.readLine())!=null) {
        System.out.println(result);
    }
    reader.close();
}

Throws丟擲異常的規則:

  • 如果是不可查異常(unchecked exception),即Error、RuntimeException或它們的子類,那麼可以不使用throws關鍵字來宣告要丟擲的異常,編譯仍能順利通過,但在執行時會被系統丟擲。
  • 必須宣告方法可丟擲的任何可查異常(checked exception)。即如果一個方法可能出現受可查異常,要麼用try-catch語句捕獲,要麼用throws子句宣告將它丟擲,否則會導致編譯錯誤
  • 僅當丟擲了異常,該方法的呼叫者才必須處理或者重新丟擲該異常。當方法的呼叫者無力處理該異常的時候,應該繼續丟擲,而不是囫圇吞棗。
  • 呼叫方法必須遵循任何可查異常的處理和宣告規則。若覆蓋一個方法,則不能宣告與覆蓋方法不同的異常。宣告的任何異常必須是被覆蓋方法所宣告異常的同類或子類。

3.3 throw-丟擲異常

如果程式碼可能會引發某種錯誤,可以建立一個合適的異常類範例並丟擲它,這就是丟擲異常。如下所示:

public static double yourMethod(int value) {
    if(value < 0) {
        throw new ArithmeticException("引數不能為0"); //丟擲一個執行時異常
    }
    return 6.0 / value;
}

大部分情況下都不需要手動丟擲異常,因為Java的大部分方法要麼已經處理異常,要麼已宣告異常。所以一般都是捕獲異常或者再往上拋。
有時我們會從 catch 中丟擲一個異常,目的是為了改變異常的型別。多用於在多系統整合時,當某個子系統故障,異常型別可能有多種,可以用統一的異常型別向外暴露,不需暴露太多內部異常細節。

private static void readFile(String filePath) throws MyException {    
    try {
        // code
    } catch (IOException e) {
        MyException ex = new MyException("read file failed.");
        ex.initCause(e);
        throw ex;
    }
}

3.4 異常的自定義

習慣上,定義一個異常類應包含兩個建構函式,一個無參建構函式和一個帶有詳細描述資訊的建構函式(Throwable 的 toString 方法會列印這些詳細資訊,偵錯時很有用), 比如上面用到的自定義
MyException:

public class MyException extends Exception {
    public MyException(){ }
    public MyException(String msg){
        super(msg);
    }
    // ...
}

3.5 異常的捕獲

異常捕獲處理的方法通常有:

  • try-catch
  • try-catch-finally
  • try-finally
  • try-with-resource

3.5.1 try-catch

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

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

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

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

3.5.2 try-catch-finally

  • 常規語法
try {                        
    //執行程式程式碼,可能會出現異常                 
} catch(Exception e) {   
    //捕獲異常並處理   
} finally {
    //必執行的程式碼
}
  • 執行的順序

    • 當try沒有捕獲到異常時:try語句塊中的語句逐一被執行,程式將跳過catch語句塊,執行finally語句塊和其後的語句;
    • 當try捕獲到異常,catch語句塊裡沒有處理此異常的情況:當try語句塊裡的某條語句出現異常時,而沒有處理此異常的catch語句塊時,此異常將會拋給JVM處理,finally語句塊裡的語句還是會被執行,但finally語句塊後的語句不會被執行;
    • 當try捕獲到異常,catch語句塊裡有處理此異常的情況:在try語句塊中是按照順序來執行的,當執行到某一條語句出現異常時,程式將跳到catch語句塊,並與catch語句塊逐一匹配,找到與之對應的處理程式,其他的catch語句塊將不會被執行,而try語句塊中,出現異常之後的語句也不會被執行,catch語句塊執行完後,執行finally語句塊裡的語句,最後執行finally語句塊後的語句;
  • 無異常情況 ,catch 模組被忽略,先執行業務邏輯,再執行finally。

  • 異常情況,假設執行到業務邏輯2的時候,出現故障異常,則業務邏輯3沒有執行,直接執行catch,最後再執行finally。

  • 一個完整的例子

private static void readFile(String filePath) throws MyException {
    File file = new File(filePath);
    String result;
    BufferedReader reader = null;
    try {
        reader = new BufferedReader(new FileReader(file));
        while((result = reader.readLine())!=null) {
            System.out.println(result);
        }
    } catch (IOException e) {
        System.out.println("readFile method catch block.");
        MyException ex = new MyException("read file failed.");
        ex.initCause(e);
        throw ex;
    } finally {
        System.out.println("readFile method finally block.");
        if (null != reader) {
            try {
                reader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

3.5.3 try-finally

也可以直接用try-finally嗎。try塊中引起異常,異常程式碼之後的語句不再執行,直接執行finally語句。
try塊沒有引發異常,則執行完try塊就執行finally語句。 try-finally可用在不需要捕獲異常的程式碼,可以保證資源在使用後被關閉。例如IO流中執行完相應操作後,關閉相應資源;使用Lock物件保證執行緒同步,通過finally可以保證鎖會被釋放;資料庫連執行緒式碼時,關閉連線操作等等。

//以Lock加鎖為例,演示try-finally
ReentrantLock lock = new ReentrantLock();
try {
    //需要加鎖的程式碼
} finally {
    lock.unlock(); //保證鎖一定被釋放
}

finally遇見如下情況不會執行

  • 在前面的程式碼中用了System.exit()退出程式。
  • finally語句塊中發生了異常。
  • 程式所在的執行緒死亡。
  • 關閉CPU。

3.5.4 try-with-resource

上面例子中,finally 中的 close 方法也可能丟擲 IOException, 從而覆蓋了原始異常。JAVA 7 提供了更優雅的方式來實現資源的自動釋放,自動釋放的資源需要是實現了 AutoCloseable 介面的類。

  • 程式碼實現
private  static void tryWithResourceTest(){
    try (Scanner scanner = new Scanner(new FileInputStream("c:/abc"),"UTF-8")){
        // code
    } catch (IOException e){
        // handle exception
    }
}
  • 看下Scanner
public final class Scanner implements Iterator<String>, Closeable {
  // ...
}
public interface Closeable extends AutoCloseable {
    public void close() throws IOException;
}

try 程式碼塊退出時,會自動呼叫 scanner.close 方法,和把 scanner.close 方法放在 finally 程式碼塊中不同的是,若 scanner.close 丟擲異常,則會被抑制,丟擲的仍然為原始異常。被抑制的異常會由 addSusppressed 方法新增到原來的異常,如果想要獲取被抑制的異常列表,可以呼叫 getSuppressed 方法來獲取。

3.6 異常總結

  • try、catch和finally都不能單獨使用,只能是try-catch、try-finally或者try-catch-finally。
  • try語句塊監控程式碼,出現異常就停止執行下面的程式碼,然後將異常移交給catch語句塊來處理。
  • finally語句塊中的程式碼一定會被執行,常用於回收資源 。
  • throws:宣告一個異常,告知方法呼叫者。
  • throw :丟擲一個異常,至於該異常被捕獲還是繼續丟擲都與它無關。
    Java程式設計思想一書中,對異常的總結。
  • 在恰當的級別處理問題。(在知道該如何處理的情況下了捕獲異常。)
  • 解決問題並且重新呼叫產生異常的方法。
  • 進行少許修補,然後繞過異常發生的地方繼續執行。
  • 用別的資料進行計算,以代替方法預計會返回的值。
  • 把當前執行環境下能做的事儘量做完,然後把相同的異常重拋到更高層。
  • 把當前執行環境下能做的事儘量做完,然後把不同的異常拋到更高層。
  • 終止程式。
  • 進行簡化(如果你的異常模式使問題變得太複雜,那麼用起來會非常痛苦)。
  • 讓類庫和程式更安全。

3.7 常用的異常

在Java中提供了一些異常用來描述經常發生的錯誤,對於這些異常,有的需要程式設計師進行捕獲處理或宣告丟擲,有的是由Java虛擬機器器自動進行捕獲處理。Java中常見的異常類:

  • RuntimeException
    • java.lang.ArrayIndexOutOfBoundsException 陣列索引越界異常。當對陣列的索引值為負數或大於等於陣列大小時丟擲。
    • java.lang.ArithmeticException 算術條件異常。譬如:整數除零等。
    • java.lang.NullPointerException 空指標異常。當應用試圖在要求使用物件的地方使用了null時,丟擲該異常。譬如:呼叫null物件的實體方法、存取null物件的屬性、計算null物件的長度、使用throw語句丟擲null等等
    • java.lang.ClassNotFoundException 找不到類異常。當應用試圖根據字串形式的類名構造類,而在遍歷CLASSPAH之後找不到對應名稱的class檔案時,丟擲該異常。
    • java.lang.NegativeArraySizeException 陣列長度為負異常
    • java.lang.ArrayStoreException 陣列中包含不相容的值丟擲的異常
    • java.lang.SecurityException 安全性異常
    • java.lang.IllegalArgumentException 非法引數異常
  • IOException
    • IOException:操作輸入流和輸出流時可能出現的異常。
    • EOFException 檔案已結束異常
    • FileNotFoundException 檔案未找到異常
  • 其他
    • ClassCastException 型別轉換異常類
    • ArrayStoreException 陣列中包含不相容的值丟擲的異常
    • SQLException 運算元據庫異常類
    • NoSuchFieldException 欄位未找到異常
    • NoSuchMethodException 方法未找到丟擲的異常
    • NumberFormatException 字串轉換為數位丟擲的異常
    • StringIndexOutOfBoundsException 字串索引超出範圍丟擲的異常
    • IllegalAccessException 不允許存取某類異常
    • InstantiationException 當應用程式試圖使用Class類中的newInstance()方法建立一個類的範例,而指定的類物件無法被範例化時,丟擲該異常

4 異常實踐

當你丟擲或捕獲異常的時候,有很多不同的情況需要考慮,而且大部分事情都是為了改善程式碼的可讀性或者 API 的可用性。
異常不僅僅是一個錯誤控制機制,也是一個通訊媒介。因此,為了和同事更好的合作,一個團隊必須要制定出一個最佳實踐和規則,只有這樣,團隊成員才能理解這些通用概念,同時在工作中使用它。
這裡給出幾個被很多團隊使用的例外處理最佳實踐。

4.1 只針對不正常的情況才使用異常

異常只應該被用於不正常的條件,它們永遠不應該被用於正常的控制流。《阿里手冊》中:【強制】Java 類庫中定義的可以通過預檢查方式規避的RuntimeException異常不應該通過catch 的方式來處理,比如:NullPointerException,IndexOutOfBoundsException等等。

比如,在解析字串形式的數位時,可能存在數位格式錯誤,不得通過catch Exception來實現

  • 程式碼1
if (obj != null) {
  //...
}
  • 程式碼2
try { 
  obj.method(); 
} catch (NullPointerException e) {
  //...
}

主要原因有三點:

  • 異常機制的設計初衷是用於不正常的情況,所以很少會會JVM實現試圖對它們的效能進行優化。所以,建立、丟擲和捕獲異常的開銷是很昂貴的。
  • 把程式碼放在try-catch中返回阻止了JVM實現本來可能要執行的某些特定的優化。
  • 對陣列進行遍歷的標準模式並不會導致冗餘的檢查,有些現代的JVM實現會將它們優化掉。

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

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

  • 錯誤範例
public void doNotCloseResourceInTry() {
    FileInputStream inputStream = null;
    try {
        File file = new File("./tmp.txt");
        inputStream = new FileInputStream(file);
        // use the inputStream to read a file
        // do NOT do this
        inputStream.close();
    } catch (FileNotFoundException e) {
        log.error(e);
    } catch (IOException e) {
        log.error(e);
    }
}

問題就是,只有沒有異常丟擲的時候,這段程式碼才可以正常工作。try 程式碼塊內程式碼會正常執行,並且資源可以正常關閉。但是,使用 try 程式碼塊是有原因的,一般呼叫一個或多個可能丟擲異常的方法,而且,你自己也可能會丟擲一個異常,這意味著程式碼可能不會執行到 try 程式碼塊的最後部分。結果就是,你並沒有關閉資源。
所以,你應該把清理工作的程式碼放到 finally 裡去,或者使用 try-with-resource 特性。

  • 方法一:使用 finally 程式碼塊
    與前面幾行 try 程式碼塊不同,finally 程式碼塊總是會被執行。不管 try 程式碼塊成功執行之後還是你在 catch 程式碼塊中處理完異常後都會執行。因此,你可以確保你清理了所有開啟的資源。
public void closeResourceInFinally() {
    FileInputStream inputStream = null;
    try {
        File file = new File("./tmp.txt");
        inputStream = new FileInputStream(file);
        // use the inputStream to read a file
    } catch (FileNotFoundException e) {
        log.error(e);
    } finally {
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (IOException e) {
                log.error(e);
            }
        }
    }
}
  • 方法二:Java 7 的 try-with-resource 語法
    如果你的資源實現了 AutoCloseable 介面,你可以使用這個語法。大多數的 Java 標準資源都繼承了這個介面。當你在 try 子句中開啟資源,資源會在 try 程式碼塊執行後或例外處理後自動關閉。
public void automaticallyCloseResource() {
    File file = new File("./tmp.txt");
    try (FileInputStream inputStream = new FileInputStream(file);) {
        // use the inputStream to read a file
    } catch (FileNotFoundException e) {
        log.error(e);
    } catch (IOException e) {
        log.error(e);
    }
}

4.3 儘量使用標準的異常

重用現有的異常有幾個好處:

  • 它使得你的API更加易於學習和使用,因為它與程式設計師原來已經熟悉的習慣用法是一致的。
  • 對於用到這些API的程式而言,它們的可讀性更好,因為它們不會充斥著程式設計師不熟悉的異常。
  • 異常類越少,意味著記憶體佔用越小,並且轉載這些類的時間開銷也越小。
    Java標準異常中有幾個是經常被使用的異常。如下表格:
異常 使用場合
IllegalArgumentException 引數的值不合適
IllegalStateException 引數的狀態不合適
NullPointerException 在null被禁止的情況下引數值為null
IndexOutOfBoundsException 下標越界
ConcurrentModificationException 在禁止並行修改的情況下,物件檢測到並行修改
UnsupportedOperationException 物件不支援客戶請求的方法

雖然它們是Java平臺庫迄今為止最常被重用的異常,但是,在許可的條件下,其它的異常也可以被重用。例如,如果你要實現諸如複數或者矩陣之類的算術物件,那麼重用ArithmeticException和NumberFormatException將是非常合適的。如果一個異常滿足你的需要,則不要猶豫,使用就可以,不過你一定要確保丟擲異常的條件與該異常的檔案中描述的條件一致。這種重用必須建立在語意的基礎上,而不是名字的基礎上。
最後,一定要清楚,選擇重用哪一種異常並沒有必須遵循的規則。例如,考慮紙牌物件的情形,假設有一個用於發牌操作的方法,它的引數(handSize)是發一手牌的紙牌張數。假設呼叫者在這個引數中傳遞的值大於整副牌的剩餘張數。那麼這種情形既可以被解釋為IllegalArgumentException(handSize的值太大),也可以被解釋為IllegalStateException(相對客戶的請求而言,紙牌物件的紙牌太少)。

4.4 對異常進行檔案說明

當在方法上宣告丟擲異常時,也需要進行檔案說明。目的是為了給呼叫者提供儘可能多的資訊,從而可以更好地避免或處理異常。
在 Javadoc 新增 @throws 宣告,並且描述丟擲異常的場景。

/**
* Method description
* 
* @throws MyBusinessException - businuess exception description
*/
public void doSomething(String input) throws MyBusinessException {
   // ...
}

同時,在丟擲MyBusinessException 異常時,需要儘可能精確地描述問題和相關資訊,這樣無論是列印到紀錄檔中還是在監控工具中,都能夠更容易被人閱讀,從而可以更好地定位具體錯誤資訊、錯誤的嚴重程度等。

4.5 優先捕獲最具體的異常

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

但問題在於,只有匹配異常的第一個 catch 塊會被執行。 因此,如果首先捕獲 IllegalArgumentException ,則永遠不會到達應該處理更具體的 NumberFormatException 的 catch 塊,因為它是 IllegalArgumentException 的子類。
總是優先捕獲最具體的異常類,並將不太具體的 catch 塊新增到列表的末尾。
你可以在下面的程式碼片斷中看到這樣一個 try-catch 語句的例子。 第一個 catch 塊處理所有 NumberFormatException 異常,第二個處理所有非 NumberFormatException 異常的IllegalArgumentException 異常。

public void catchMostSpecificExceptionFirst() {
    try {
        doSomething("A message");
    } catch (NumberFormatException e) {
        log.error(e);
    } catch (IllegalArgumentException e) {
        log.error(e)
    }
}

4.6 不要捕獲 Throwable 類

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

如果在 catch 子句中使用 Throwable ,它不僅會捕獲所有異常,也將捕獲所有的錯誤。JVM 丟擲錯誤,指出不應該由應用程式處理的嚴重問題。 典型的例子是 OutOfMemoryError 或者 StackOverflowError 。兩者都是由應用程式控制之外的情況引起的,無法處理。
所以,最好不要捕獲 Throwable ,除非你確定自己處於一種特殊的情況下能夠處理錯誤。

public void doNotCatchThrowable() {
    try {
        // do something
    } catch (Throwable t) {
        // don't do this!
    }
}

4.7 不要忽略異常

很多時候,開發者很有自信不會丟擲異常,因此寫了一個catch塊,但是沒有做任何處理或者記錄紀錄檔。

public void doNotIgnoreExceptions() {
    try {
        // do something
    } catch (NumberFormatException e) {
        // this will never happen
    }
}

但現實是經常會出現無法預料的異常,或者無法確定這裡的程式碼未來是不是會改動(刪除了阻止異常丟擲的程式碼),而此時由於異常被捕獲,使得無法拿到足夠的錯誤資訊來定位問題。
合理的做法是至少要記錄異常的資訊。

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

4.8不要記錄並丟擲異常

這可能是本文中最常被忽略的最佳實踐。 可以發現很多程式碼甚至類庫中都會有捕獲異常、記錄紀錄檔並再次丟擲的邏輯。如下:

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

這個處理邏輯看著是合理的。但這經常會給同一個異常輸出多條紀錄檔。如下:

17:44:28,945 ERROR TestExceptionHandling:65 - java.lang.NumberFormatException: For input string: "xyz"
Exception in thread "main" java.lang.NumberFormatException: For input string: "xyz"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:589)
at java.lang.Long.(Long.java:965)
at com.stackify.example.TestExceptionHandling.logAndThrowException(TestExceptionHandling.java:63)
at com.stackify.example.TestExceptionHandling.main(TestExceptionHandling.java:58)

如上所示,後面的紀錄檔也沒有附加更有用的資訊。如果想要提供更加有用的資訊,那麼可以將異常包裝為自定義異常。

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

因此,僅僅當想要處理異常時才去捕獲,否則只需要在方法簽名中宣告讓呼叫者去處理。

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

捕獲標準異常幷包裝為自定義異常是一個很常見的做法。這樣可以新增更為具體的異常資訊並能夠做針對的例外處理。
在你這樣做時,請確保將原始異常設定為原因(注:參考下方程式碼 NumberFormatException e 中的原始異常 e )。Exception 類提供了特殊的建構函式方法,它接受一個 Throwable 作為引數。否則,你將會丟失堆疊跟蹤和原始異常的訊息,這將會使分析導致異常的異常事件變得困難。

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

4.10 不要使用異常控制程式的流程

不應該使用異常控制應用的執行流程,例如,本應該使用if語句進行條件判斷的情況下,你卻使用例外處理,這是非常不好的習慣,會嚴重影響應用的效能。

4.11 不要在finally塊中使用return。

try塊中的return語句執行成功後,並不馬上返回,而是繼續執行finally塊中的語句,如果此處存在return語句,則在此直接返回,無情丟棄掉try塊中的返回點。
如下是一個反例:

private int x = 0;
public int checkReturn() {
    try {
        // x等於1,此處不返回
        return ++x;
    } finally {
        // 返回的結果是2
        return ++x;
    }
}

5 總結

這邊詳細介紹了異常的概念、原理,以及在應用中的一些小結。異常的能力是我們快速定位程式錯誤的重要手段之一,也是我們不斷優化程式,提高程式健壯性的依據,所以熟練掌握異常的使用是非常有必要的。