天天看點

9種Java異常處理的最佳實踐

翻譯自:https://dzone.com/articles/9-best-practices-to-handle-exceptions-in-java

作者注:無論你是一名新手或者是一名有經驗的專業人士,經常溫習一下異常處理的優秀實踐能讓你和你的團隊更好的解決異常相關問題。

Java中的異常處理不是一個簡單的主題。初學者覺得它難以了解,甚者有經驗的開發者也需要花費數小時時間讨論如何抛出或者處理某一個異常。

是以,大多數開發團隊都會有自己的異常處理規則。如果你剛剛加入到團隊,你會驚奇的發現這些規則與你之前使用的是多麼的不同。

不同團隊使用的異常處理的優秀實踐有很多。下面的九種最佳實踐能夠能讓你更好的認識異常處理或者提升異常處理的能力。

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

在try語句塊中,使用諸如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-resrouce語句。

使用finally語句塊

與上面try語句塊的最後幾行相比,無論try語句塊正常執行或者你在catch語句塊中執行異常處理,finally語句塊確定語句總是得以執行。是以,你可以確定打開的資源得以清理。

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’s try-with-resource語句

另一個選擇是使用try-with-resource語句塊,在Java異常處理簡介中有詳細的介紹。

如果你的資源類實作了AutoCloseable接口,你便可以使用try-with-resource語句塊。大多數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);
    }
}
           

2.優先使用更為具體的異常

抛出的異常越具體越好。需要牢記的是,你的同僚可能不熟悉你的代碼,或者你在數月之後需要調用自己的方法并處理異常。

是以,要確定提供盡可能多的資訊,這樣使你的API更易于了解。這樣做以後,調用者使用你提供的方法時,能更好的處理異常或者避免進行不必要的檢查。

是以,嘗試找出最比對抛出異常事件的類,例如,抛出NumberFormatException而不是IllegalArumentExcetion。同時,盡量避免抛出不具體的異常。

public void doNotDoThis() throws Exception {
    ...
}

public void doThis() throws NumberFormatException {
    ...
}
           

3.為異常提供詳細的文檔說明

當你在方法簽名中指定異常時,也應該在Javadoc中提供文檔說明。這與上面的最佳實踐目的相同:盡可能多的為調用者提供資訊,使調用者可以避免或者處理異常。

是以,確定在javadoc中添加@throws聲明并且描述引發異常的具體場景。

/**
 * This method does something extremely useful ...
 *
 * @param input
 * @throws MyBusinessException if ... happens
 */
public void doSomething(String input) throws MyBusinessException {
    ...
}
           

4.抛出具有描述資訊的異常

本條最佳實踐的想法與上兩條相同。但是,在這條實踐中不是向調用你方法的人提供資訊。當異常消息出現在日志檔案或者監控工具中時,它可以被任何人閱讀。

是以,為了更好的了解異常事件,應該盡可能精确的描述問題,并提供最為相關的資訊。

不要誤解我:你不需要寫成段的文字描述,而應該用1-2句話介紹異常産生的原因。這有助于你的團隊更好的了解問題的嚴重性,并且讓你能更為簡單地分析任何服務。

如果抛出一個具體的異常,異常的類名通常已經描述了錯誤的類型。是以,你不需要提供額外的資訊。一個好的例子是NumberFormatException。當你提供錯誤的String類型給java.lang.Long的構造函數時,會抛出這個異常。

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

NumberFormatException的名字已經告訴你問題的類型。描述資訊隻需要提供引起異常的輸入字元串。如果異常類的名字描述不是很具體,你需要在消息中提供更為詳實的資訊。

5.優先捕獲最比對的異常

許多IDE能夠幫助你踐行本條最佳實踐。當你優先捕獲比對資訊較少的異常時,IDE會提示代碼可能得不到執行。

問題的關鍵在于,隻有第一條比對異常的catch語句塊得以執行。是以,如果你優先捕獲IllegalArgumentException異常,那麼更具體的異常NumberFormatException處理代碼永遠得不到執行,因為NumberFromatException是IllegalArgumentException的子類。

牢記優先捕獲最為比對的異常,将不具體的異常放到捕獲清單的後面。

如下面的代碼片,第一個catch語句捕獲處理NumberFormatExceptions,然後是IllegalArgumentExceptions。

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

6.不要捕獲Throwable

Throwable是異常類Exception和錯誤類Error類共同的基類。在catch語句中可以進行捕獲,但絕不要這樣做。

如果你在catch語句塊中使用Throwable,程式将會捕獲所有異常和錯誤。JVM抛出的錯誤用于表明系統的嚴重錯誤,而不是讓應用程式進行捕獲處理的。典型的例子是OutOfMemoryError或者StackOverflowError。它們都是由于程式不可控導緻的錯誤,且不應該由程式捕獲處理。

是以,除非你确信你能夠或者需要處理錯誤,最好不要捕獲Throwable。

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

7.不要忽略異常

你曾經分析過隻執行一部分用例的錯誤報告嗎?

這往往是由于忽略異常引起的。開發者可以十分确定代碼不會抛出異常并且添加了不做任何處理或者僅記錄日志的捕獲異常處理。當你看到這段代碼時,你會發現那句有名的注釋:“這永遠不會發生”。

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);
    }
}
           

8. 不要同時記錄日志和抛出異常

這可能是最容易被忽略的最佳實踐。你可能見過許多這樣的代碼,甚至在某些庫代碼中,捕獲異常後,既記錄日志又重新抛出異常。

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

異常發生時記錄日志然後重新抛出以便調用者處理,你可能覺得這樣很直覺。但這會導緻一個異常寫了兩次錯誤日志。

::, ERROR TestExceptionHandling: - 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:)
at java.lang.Long.parseLong(Long.java:)
at java.lang.Long.(Long.java:)
at com.stackify.example.TestExceptionHandling.logAndThrowException(TestExceptionHandling.java:)
at com.stackify.example.TestExceptionHandling.main(TestExceptionHandling.java:)
           

這些額外的資訊沒有添加任何有用的資訊。正如在第四條最佳實踐中所說的,異常消息應該較長的描述異常事件。調用棧已經告訴你所抛出異常的類、方法和代碼行。

如果你需要添加額外的資訊,你應該捕獲異常然後重新包裝它。但確定遵守第九條最佳實踐。

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

是以,僅捕獲你想處理的異常。否則,在方法簽名中标明并讓調用者處理它。

9. 包裝異常而不是消費異常

有時捕獲基本異常然後将其包裝為自定義異常的做法是值得推薦的。這種異常的一個典型的例子是應用或者架構抛出的具體業務異常。它們允許你添加額外的資訊,并實作自定義的異常處理類。

當你這樣做時,確定保留原有的異常。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);
    }
}
           

總結

如上所述,當你抛出或者捕獲異常時,有許多不同的事情需要考慮。其中的大多數事情是為了提高你代碼的可讀性和API的易用性。

異常處理同時涉及錯誤處理機制和事件傳遞機制。是以,你應該與你的同僚讨論異常處理的最佳實踐,進而讓每個人了解異常的基本概念并采用相同的方法使用異常處理。

PS:簡書位址連結:

https://www.jianshu.com/p/1948dc648f15