天天看點

Java中處理異常的9個最佳實踐

原文:9 Best Practices to Handle Exceptions in Java

作者:Thorben Janssen

譯者:Teixeira10

【譯者注】在本文中,作者介紹了9個處理異常的最佳方法與實踐,以舉例與代碼展示結合的方式,讓開發者更好的了解這9種方式,并指導讀者在不同情況下選擇不同的異常處理方式。

以下為譯文:

Java中的異常處理不是一個簡單的話題。初學者很難了解,甚至有經驗的開發人員也會花幾個小時來讨論應該如何抛出或處理這些異常。

這就是為什麼大多數開發團隊都有自己的異常處理的規則和方法。如果你是一個團隊的新手,你可能會驚訝于這些方法與你之前使用過的那些方法有多麼不同。

然而,有幾種異常處理的最佳方法被大多數開發團隊所使用。下面是幫助改進異常處理的9個最重要的方法。

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-Resource語句。

使用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的Try-With-Resource語句

另一個選擇是Try-With-Resource語句,在introduction to Java exception handling中更詳細地說明了這一點。

如果你的資源實作了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);
    }
}
           

2. 給出準确的異常處理資訊

你抛出的異常越具體越好。一定要記住,一個不太了解你代碼的同僚,也許幾個月後,需要調用你的方法,并且處理這個異常。

是以,請確定提供盡可能多的資訊,這會使你的API更容易了解。是以,你方法的調用者将能夠更好地處理異常,或者通過額外的檢查來避免它。

是以,要盡量能更好地描述你的異常處理資訊,比如用NumberFormatException代替IllegalArgumentException,避免抛出一個不具體的異常。

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,它就會被類 java.lang.Long的構造函數抛出。

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

NumberFormatException已經告訴你問題的類型,是以隻需要提供導緻問題的輸入字元串。如果異常類的名稱不具有表達性,那麼就需要提供必要的解釋資訊。

17:17:26,386 ERROR TestExceptionHandling:52 - java.lang.NumberFormatException: For input string: "xyz"
           

5. 最先捕獲特定的異常

大多數IDE都可以幫助你做到這點,當你試圖捕獲不确定的異常時,它會報告一個不可到達的代碼塊。

問題是隻有第一個比對到異常的catch語句才會被執行,是以,如果你最先發現IllegalArgumentException,你将永遠不會到達catch裡處理更具體的NumberFormatException,因為它是IllegalArgumentException的一個子類。

是以要首先捕獲特定的異常類,并在末尾添加一些處理不是很具體異常的catch語句。

你可以在下面的代碼片段中看到這樣一個try-catch語句的示例。第一個catch處理所有NumberFormatExceptions異常,第二個catch 處理NumberFormatException異常以外的illegalargumentexception異常。

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

6. 不要在catch中使用Throwable

Throwable是exceptions 和 errors的父類。當然,你可以在catch子句中使用它,但其實你不應該這樣做。

如果你在catch子句中使用Throwable,它将不僅捕獲所有的異常,還會捕獲所有錯誤。JVM會抛出錯誤,這是應用程式不打算處理的嚴重問題。典型的例子是OutOfMemoryError或StackOverflowError。這兩種情況都是由應用程式控制之外的情況引起的,無法處理。

是以,最好不要在catch中使用Throwable,除非你完全确定自己處于一個特殊的情況下,并且你需要處理一個錯誤。

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

7. 不要忽略Exceptions

你是否曾經分析過隻有用例的第一部分才被執行的bug報告嗎?

這通常是由一個被忽略的異常引起的。開發人員可能非常确信它不會被抛出,并添加一個無法處理或無法記錄它的catch語句。當你發現它的時候,你很可能就會明白一句著名的話“This will never happen”。

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

當它發生時記錄一個異常,然後重新抛出它,以便調用者能夠适當地處理它,這可能會很直覺。但是它會為同一個異常寫多個錯誤消息。

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)
           

不添加任何額外的資訊。正如在上述第4個中所解釋的那樣,異常消息應該描述異常事件。堆棧會告訴你在哪個類、方法和行中異常被抛出。

如果你需要添加額外的資訊,應該捕獲異常并将其包裝在一個自定義的資訊中。但要確定遵循下面的第9條。

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可用性。

異常通常是一個錯誤處理機制和一個通信媒介。是以,你應該確定同僚一起讨論想要應用的最佳實踐和方法,以便每個人都了解通用概念并以相同的方式使用它們。

繼續閱讀