其實工作這么久了一直都沒搞清楚到底如何來處理異常,偶然看到一篇外文感覺還不錯,便把它翻譯了下來,原文鏈接位於本文末尾處。
在java中處理異常並不是一件簡單的事,不止初學者覺得它難以理解甚至連有經驗的開發者也會花費幾個小時來討論某個異常應該拋出還是處理掉。
這就是為何大多數開發團隊都擁有自己的規范來指明如何使用它們,如果你剛來到一個新的團隊,你可能會發現新團隊的准則與你之前遵循的大有不同。
盡管如此,這里還是有幾條最佳准則被大多數團隊所遵循。這里有9條准則可以幫助你提高處理異常的水平。
1、在Finally塊中清理資源或者使用Try-With-Resource語句
在try塊中使用資源的情況在開發中會經常碰到,比如一個InputStream,使用它之后你需要將它關閉。在這種情況下經常會看到在try塊中去關閉資源的錯誤。
1 public void doNotCloseResourceInTry() { 2 FileInputStream inputStream = null; 3 try { 4 File file = new File("./tmp.txt"); 5 inputStream = new FileInputStream(file); 6 7 // use the inputStream to read a file 8 9 // do NOT do this 10 inputStream.close(); 11 } catch (FileNotFoundException e) { 12 log.error(e); 13 } catch (IOException e) { 14 log.error(e); 15 } 16 }
這樣寫在沒有異常拋出的情況下似乎運行得非常溜,所有在try塊下的語句都會被正常執行,且資源都會被關閉。
但使用try塊是有它的原因的,你調用的一個或者多個方法可能會拋出異常,或者你自己主動拋出異常,這意味着try塊中的語句可能會無法完整的執行,最終導致資源沒有關閉。
使用Finally塊
與try塊不同的是——finally塊中的語句總是會被執行,無論是try塊中的語句成功執行還是你在catch塊中處理了一個異常。因此所有開啟的資源都能夠確保被關閉。
1 public void closeResourceInFinally() { 2 FileInputStream inputStream = null; 3 try { 4 File file = new File("./tmp.txt"); 5 inputStream = new FileInputStream(file); 6 7 // use the inputStream to read a file 8 9 } catch (FileNotFoundException e) { 10 log.error(e); 11 } finally { 12 if (inputStream != null) { 13 try { 14 inputStream.close(); 15 } catch (IOException e) { 16 log.error(e); 17 } 18 } 19 } 20 }
Java 7的Try-With-Resource語句
還有一種選擇便是使用try-with-resource,與之相關的詳情在我的另一邊文章——introduction to Java exception handling中有介紹。
如果你的資源實現了AutoCloseable接口的話你便可以使用try-with-resource語句,這也是大多數Java標准資源的做法,如果你在try-with-resource語句中聲明了一個資源,它將會在try塊中的語句執行后或者將異常處理后自動關閉。
1 public void automaticallyCloseResource() { 2 File file = new File("./tmp.txt"); 3 try (FileInputStream inputStream = new FileInputStream(file);) { 4 // use the inputStream to read a file 5 6 } catch (FileNotFoundException e) { 7 log.error(e); 8 } catch (IOException e) { 9 log.error(e); 10 } 11 }
2、優先使用更明確的異常
拋出的異常越明確越好,你要想着你一個不了解你的代碼的同事或者你在幾個月后,需要調用你的方法並且處理異常。
因此要確保提供盡可能多的信息,使你的API更容易被理解,使得該方法的調用者能更好的處理異常且避免額外的檢查。
所以,應該尋找與你的異常事件最貼切的類,例如:拋出一個NumberFormatException而不是IllegalArgumentException(譯者注:這句話缺少上下文,不明白作者的意思)。且避免拋出不明確的異常。
1 public void doNotDoThis() throws Exception { ... } 2 3 public void doThis() throws NumberFormatException { ... }
3、在文檔里記錄你的異常
每當你在方法簽名處指定一個異常,都應該同時將它記錄到Javadoc里邊。
這與前一條准則的目的是一樣的:給方法調用者提供盡可能多的信息,讓他可以避免觸發異常或者方便的他處理異常。
所以確保要在Javadoc里邊添加@throws聲明並且描述什么樣的情況會導致異常。
1 /** 2 * This method does something extremely useful ... 3 * 4 * @param input 5 * @throws MyBusinessException if ... happens 6 */ 7 public void doSomething(String input) throws MyBusinessException { ... }
4、將異常與它的描述信息一並拋出
這條准則的想法與前兩條是相同的,但這次你不是給你的方法調用者提供信息,當這個異常信息被打印到日志文件或者反饋到你的監視工具時,它要能被每個想要了解發生了什么的人理解。
因此,我們應該盡可能精確的描述問題並且提供更接地氣的消息來讓他人理解發生了什么異常。
別誤會我的意思,你沒必因此寫上一大段話,但你應該用一兩句話簡明扼要的解釋一下異常的原因。以幫助你的運營團隊了解發生了什么問題,同時這也會使你更容易分析問題原因。
如果你拋出一個明確的異常,它的類名很可能已經描述了這是怎樣一個錯誤了,所以你不需要提供大量格外的信息,NumberFormatException就是一個很好的例子,當你給java.lang.Long的構造函數提供一個錯誤格式的String類型參數時便會拋出NumberFormatException。
1 try { 2 new Long("xyz"); 3 } catch (NumberFormatException e) { 4 log.error(e); 5 }
NumberFormatException的類名已經告訴你這是什么類型的問題了,它的異常消息只需要指明導致這個問題的輸入字符串,如果異常類的名稱不能達其意,你需要在異常消息中提供必要的信息。
1 17:17:26,386 ERROR TestExceptionHandling:52 - java.lang.NumberFormatException: For input string: "xyz"
5、優先捕獲更明確的異常
大部分IDE都會幫你遵循這條准則,當你把沒那么明確的異常放在前面的時候他們會提示存在無法到達的代碼塊。
這是因為只有第一個匹配到的catch塊才會被執行,所以如果你先捕獲IllegalFormatException,你將永遠無法到達處理NumberFormatException的catch塊,因為NumberFormatException是IllegalArgumentException的子類。
所以應優先捕獲明確的異常,將沒那么明確的catch塊放在后邊。
如下代碼片段中的try-catch語句。第一個catch塊處理所有NumberFormatException,第二個則處理所有非NumberFormatException的IllegalArgumentException。
1 public void catchMostSpecificExceptionFirst() { 2 try { 3 doSomething("A message"); 4 } catch (NumberFormatException e) { 5 log.error(e); 6 } catch (IllegalArgumentException e) { 7 log.error(e) 8 } 9 }
6、不要捕獲Throwable
Throwable是所有異常(Exception)和錯誤(Error)的父類,雖然它能在catch從句中使用,但永遠都不要這樣做!
如果你在catch從句中使用了Throwable,它將不僅捕獲所有異常,它還將捕獲所有錯誤,錯誤是由JVM拋出的,用來表明不打算讓應用來處理的嚴重錯誤。
OutOfMemoryError和StackOverflowError便是典型的例子,它們都是由於一些超出應用處理范圍的情況導致的。
1 public void doNotCatchThrowable() { 2 try { 3 // do something 4 } catch (Throwable t) { 5 // don't do this! 6 } 7 }
7、別忽略異常
你是否曾經分析過一份不完整的bug報告?
這通常是由於忽略異常導致的,這開發者大概很確定這里永遠都不會拋出異常並加了一個不處理且不打印日志的catch塊,當你找到這個塊時,甚至很可能發現這么一句著名的注釋——“This will never happen”
1 public void doNotIgnoreExceptions() { 2 try { 3 // do something 4 } catch (NumberFormatException e) { 5 // this will never happen 6 } 7 }
好吧,你可能正在分析一個不可能發生的問題。
所以,請不要忽略異常,你不知道代碼在將來會如何變動,可能有人會將防止該異常事件的校驗移除掉且沒有意識到這會產生問題,或者拋出異常的這段代碼改變了,相同的類現在變成拋出多個異常,而調用它的代碼沒有預防所有的異常。
你至少要將日志打印出來告訴別人這里發生了異常,方便別人來檢查。
1 public void logAnException() { 2 try { 3 // do something 4 } catch (NumberFormatException e) { 5 log.error("This should never happen: " + e); 6 } 7 }
8、不要打印異常日志的同時將其拋出
這可能是本文當中最常被忽視一條准則,你會在很多代碼片段中甚至庫中發現一個異常被捕獲打印日志后被重新拋出。
1 try { 2 new Long("xyz"); 3 } catch (NumberFormatException e) { 4 log.error(e); 5 throw e; 6 }
這樣做可能確實是直觀的看到了異常日志,然后將異常重新拋出,所以調用者也能正確的處理異常,但這樣做會使一個異常打印多個異常信息。
1 17:44:28,945 ERROR TestExceptionHandling:65 - java.lang.NumberFormatException: For input string: "xyz" 2 Exception in thread "main" java.lang.NumberFormatException: For input string: "xyz" 3 at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) 4 at java.lang.Long.parseLong(Long.java:589) 5 at java.lang.Long.(Long.java:965) 6 at com.stackify.example.TestExceptionHandling.logAndThrowException(TestExceptionHandling.java:63) 7 at com.stackify.example.TestExceptionHandling.main(TestExceptionHandling.java:58)
且額外的消息並沒有提供任何有用的信息,根據第4條准則,異常消息應該描述異常事件,堆棧信息告訴你異常拋出的所在類、方法、行數。
如果你需要添加額外的信息,你應該將異常捕獲並將其包在你的自定義異常中,但要確保遵循第9條准則。
1 public void wrapException(String input) throws MyBusinessException { 2 try { 3 // do something 4 } catch (NumberFormatException e) { 5 throw new MyBusinessException("A message that describes the error.", e); 6 } 7 }
所以只有在你想要處理某個異常的時候才應該去捕獲它,否則在方法簽名處聲明拋出該異常讓調用者去關注它就好了。
9、包裹某個異常的同時不要丟棄它原本的信息
有時候我們需要捕獲一個標准異常並用自定義異常包裹住它,一個典型的例子便是比如某個應用或者框架的指明業務異常,它允許你添加額外的信息,你也可以實現特別的異常處理方法。
當你這么做的時候,要確保將原本的異常作為原因設置到自定義異常里面,Exception類提供指定的構造方法可以接收Throwable類的對象作為參數,否則你將會丟失堆棧信息和原異常的消息,這將會令異常分析變得什么的困難。
1 public void wrapException(String input) throws MyBusinessException { 2 try { 3 // do something 4 } catch (NumberFormatException e) { 5 throw new MyBusinessException("A message that describes the error.", e); 6 } 7 }
總結
如你所見,當你捕獲或者拋出異常的時候你需要考慮很多事情,總的來說這些准則都是為了提高代碼的可讀性,API的可用性。
異常往往既是是錯誤處理機制也是溝通媒介,因此,你應該與你的同事一起討論這些准則,使每個人都理解這些常規的概念,並以相同的風格在實踐中應用它們。
原文鏈接:https://stackify.com/best-practices-exceptions-java/
譯者博客:http://www.cnblogs.com/kcher90/