Java中的異常處理不是一個簡單的話題。初學者很難理解,甚至有經驗的開發人員也會花幾個小時來討論應該如何拋出或處理這些異常。java
這就是爲何大多數開發團隊都有本身的異常處理的規則和方法。若是你是一個團隊的新手,你可能會驚訝於這些方法與你以前使用過的那些方法有多麼不一樣。app
然而,有幾種異常處理的最佳方法被大多數開發團隊所使用。下面是幫助改進異常處理的9個最重要的方法。框架
一般狀況下,你在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的末尾。所以,將不會關閉這些資源。this
因此應該將清理資源的代碼放入Finally中,或者使用Try-With-Resource語句。spa
使用Finally.net
相比於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語句code
另外一個選擇是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); } }
你拋出的異常越具體越好。必定要記住,一個不太瞭解你代碼的同事,也許幾個月後,須要調用你的方法,而且處理這個異常。
所以,請確保提供儘量多的信息,這會使你的API更容易理解。所以,你方法的調用者將可以更好地處理異常,或者經過額外的檢查來避免它。
因此,要儘可能能更好地描述你的異常處理信息,好比用 NumberFormatException 代替 IllegalArgumentException ,避免拋出一個不具體的異常。
public void doNotDoThis() throws Exception { ... } public void doThis() throws NumberFormatException { ... }
當你在方法中指定一個異常時,你應該在Javadoc中記錄下它。這與前面提到的方法有着相同的目標:爲調用者提供儘量多的信息,這樣他們就能夠避免異常或者更容易地處理異常。
所以,請確保在Javadoc中添加一個@throws 聲明,並描述可能致使的異常狀況。
/** * This method does something extremely useful ... * * @param input * @throws MyBusinessException if ... happens */ public void doSomething(String input) throws MyBusinessException { ... }
這一最佳實踐的理念與前兩個類似。但這一次,你不用給調用方法的人提供信息。異常消息會被全部人讀取,同時必須瞭解在日誌文件或監視工具中報告異常時發生了什麼。
所以,應該儘量準確地描述問題,並提供相關的信息來了解異常事件。
別誤會,你不須要寫一段文字,而是應該用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"
大多數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) } }
Throwable 是exceptions 和 errors的父類。固然,你能夠在catch子句中使用它,但其實你不該該這樣作。
若是你在catch子句中使用Throwable,它將不只捕獲全部的異常,還會捕獲全部錯誤。JVM會拋出錯誤,這是應用程序不打算處理的嚴重問題。典型的例子是 OutOfMemoryError 或 StackOverflowError 。這兩種狀況都是由應用程序控制以外的狀況引發的,沒法處理。
因此,最好不要在catch中使用Throwable,除非你徹底肯定本身處於一個特殊的狀況下,而且你須要處理一個錯誤。
public void doNotCatchThrowable() { try { // do something } catch (Throwable t) { // don't do this! } }
你是否曾經分析過只有用例的第一部分才被執行的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); } }
這多是最常被忽略的。你能夠在許多代碼片斷或者庫文件裏發現,有異常會被捕獲、記錄和從新拋出。
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); } }
所以,只須要捕獲一個你想要處理的異常,在方法中指定它,並讓調用者處理它。
有時最好捕獲一個標準異常並將其封裝到一個定製的異常中。此類異常的典型例子是應用程序或框架特定的業務異常。這容許你添加額外的信息,而且也能夠爲異常類實現一個特殊的處理。
當你這樣作時,確保引用原始的異常處理。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可用性。
異常一般是一個錯誤處理機制和一個通訊媒介。所以,你應該確保同事一塊兒討論想要應用的最佳實踐和方法,以便每一個人都理解通用概念並以相同的方式使用它們。