Java Exception 處理之最佳實踐
本文是Exception處理的一篇不錯的文章,從Java Exception的概念介紹起,依次講解了Exception的型別(Checked/Unchecked),Exception處理的最佳實現:
1. 選擇Checked還是Unchecked的幾個經典依據
2. Exception的封裝問題
3. 如無必要不要建立自己的Exception
4. 不要用Exception來作流程控制
5. 不要輕易的忽略捕獲的Exception
6. 不要簡單地捕獲頂層的Exception
原文地址:
http://www.onjava.com/pub/a/onjava/2003/11/19/exceptions.html
關於異常處理的一個問題就是要對何時(when)和如何(how)使用它們做到了然於心。在本文中我將介紹一些關於異常處理的最佳實踐,同時我也會涉及到最近爭論十分激烈的checked Exception的使用問題。
作為開發員,我們都希望能寫出解決問題並且是高質量的程式碼。不幸的是,一些副作用(side effects)伴隨著異常在我們的程式碼中慢慢滋生。無庸置疑,沒有人喜歡副作用(side effects),所以我們很快就用我們自己的方式來避免它,我曾經看到一些聰明的程式設計師用下面的方式來處理異常:
public void consumeAndForgetAllExceptions(){
try {
...some code that throws exceptions
} catch (Exception ex){
ex.printStacktrace();
}
}
上邊的程式碼有什麼問題麼?
在回答以前讓我們想想怎樣才是正確的?是的,一旦程式碰到異常,它就該掛起程式而"做"點什麼。那麼上邊的程式碼是這樣子的麼?看吧,它隱瞞了什麼?它把所有的"苦水"往肚裡咽(在控制檯打印出異常資訊),然後一切繼續,從表面上看就像什麼都沒有發生過一樣......,很顯然,上邊程式碼達到的效果並不是我們所期望的。
後來又怎樣?
public void someMethod() throws Exception{
}
上邊的程式碼又有什麼問題?
很明顯,上邊的方法體是空的,它不實現任何的功能(沒有一句程式碼),試問一個空方法體能丟擲什麼異常?當然Java並不阻止你這麼幹。最近,我也遇到類似的情景,方法宣告會丟擲異常,但是程式碼中並沒有任何"機會"來"展示"異常。當我問開發員為什麼要這樣做的時候,他回答我說"我知道,它確實有點那個,但我以前就是這麼幹的並且它確實能為我工作。"
在C++社群曾經花了數年實踐來實踐如何使用異常,關於此類的爭論在 java社群才剛剛開始。我曾經看到許多Java程式設計師針對使用異常的問題進行爭論。如果對於異常處理不當的話,異常可以大大減慢應用程式的執行速度,因為它將消耗記憶體和CPU來建立、丟擲並捕獲異常。如果過分的依賴異常處理,程式碼對易讀和易使用這兩方面產生影響,以至於會讓我們寫出上邊兩處"糟糕"程式碼。
異常原理
大體上說,有三種不同的"情景"會導致異常的丟擲:
l 程式設計錯誤導致異常(Exception due Programming errors): 這種情景下,異常往往處於程式設計錯誤(如:NullPointerException 或者 IllegalArgumentException),這時異常一旦丟擲,客戶端將變得無能為力。
l 客戶端程式碼錯誤導致異常(Exception due client code errors): 說白點就是客戶端試圖呼叫API不允許的操作。
l 資源失敗導致異常(Exception due to resource failures): 如記憶體不足或網路連線失敗導致出現異常等。這些異常的出現客戶端可以採取相應的措施來恢復應用程式的繼續執行。
Java中異常的型別
Java 中定義了兩類異常:
l Checked exception: 這類異常都是Exception的子類
l Unchecked exception: 這類異常都是RuntimeException的子類,雖然RuntimeException同樣也是Exception的子類,但是它們是特殊的,它們不能通過client code來試圖解決,所以稱為Unchecked exception
舉個例子,下圖為NullPointerException的繼承關係:
圖中,NullPointerException繼承自RuntimeException,所以它是Unchecked exception.
以往我都是應用checked exception多於Unchecked exception,最近,在java社群激起了一場關於checked exception和使用它們的價值的爭論。這場爭論起源於JAVA是第一個擁有Checked exception的主流OO語言這樣一個事實,而C++和C#都是根本沒有Checked exception,它們所有的異常都是unchecked。
一個checked exception強迫它的客戶端可以丟擲並捕獲它,一旦客戶端不能有效地處理這些被丟擲的異常就會給程式的執行帶來不期望的負擔。
Checked exception還可能帶來封裝洩漏,看下面的程式碼:
public List getAllAccounts() throws
FileNotFoundException, SQLException{
...
}
上邊的方法丟擲兩個異常。客戶端必須顯示的對這兩種異常進行捕獲和處理即使是在完全不知道這種異常到底是因為檔案還是資料庫操作引起的情況下。因此,此時的異常處理將導致一種方法和呼叫之間不合適的耦合。
接下來我會給出幾種設計異常的最佳實踐 (Best Practises for Designing the API)
1. 當要決定是採用checked exception還是Unchecked exception的時候,你要問自己一個問題,"如果這種異常一旦丟擲,客戶端會做怎樣的補救?"
如果客戶端可以通過其他的方法恢復異常,那麼這種異常就是checked exception;如果客戶端對出現的這種異常無能為力,那麼這種異常就是Unchecked exception;從使用上講,當異常出現的時候要做一些試圖恢復它的動作而不要僅僅的列印它的資訊,總來的來說,看下錶:
Client's reaction when exception happens
Exception type
Client code cannot do anything
Make it an unchecked exception
Client code will take some useful recovery action based on information in exception
Make it a checked exception
此外,儘量使用unchecked exception來處理程式設計錯誤:因為unchecked exception不用使客戶端程式碼顯示的處理它們,它們自己會在出現的地方掛起程式並打印出異常資訊。Java API中提供了豐富的unchecked excetpion,譬如:NullPointerException , IllegalArgumentException 和 IllegalStateException等,因此我一般使用這些標準的異常類而不願親自建立新的異常類,這樣使我的程式碼易於理解並避免的過多的消耗記憶體。
2. 保護封裝性(Preserve encapsulation)
不要讓你要丟擲的checked exception升級到較高的層次。例如,不要讓SQLException延伸到業務層。業務層並不需要(不關心?)SQLException。你有兩種方法來解決這種問題:
l 轉變SQLException為另外一個checked exception,如果客戶端並不需要恢復這種異常的話;
l 轉變SQLException為一個unchecked exception,如果客戶端對這種異常無能為力的話;
多數情況下,客戶端程式碼都是對SQLException無能為力的,因此你要毫不猶豫的把它轉變為一個unchecked exception,看看下邊的程式碼:
public void dataAccessCode(){
try{
..some code that throws SQLException
}catch(SQLException ex){
ex.printStacktrace();
}
}
上邊的catch塊緊緊列印異常資訊而沒有任何的直接操作,這是情有可原的,因為對於SQLException你還奢望客戶端做些什麼呢?(但是顯然這種就象什麼事情都沒發生一樣的做法是不可取的)那麼有沒有另外一種更加可行的方法呢?
public void dataAccessCode(){
try{
..some code that throws SQLException
}catch(SQLException ex){
throw new RuntimeException(ex);
}
}
上邊的做法是把SQLException轉換為RuntimeException,一旦SQLException被丟擲,那麼程式將丟擲RuntimeException,此時程式被掛起並返回客戶端異常資訊。
如果你有足夠的信心恢復它當SQLException被丟擲的時候,那麼你也可以把它轉換為一個有意義的checked exception, 但是我發現在大多時候丟擲RuntimeException已經足夠用了。
3. 不要建立沒有意義的異常(Try not to create new custom exceptions if they do not have useful information for client code.)
看看下面的程式碼有什麼問題?
public class DuplicateUsernameException
extends Exception {}
它除了有一個"意義明確"的名字以外沒有任何有用的資訊了。不要忘記Exception跟其他的Java類一樣,客戶端可以呼叫其中的方法來得到更多的資訊。
我們可以為其新增一些必要的方法,如下:
public class DuplicateUsernameException
extends Exception {
public DuplicateUsernameException
(String username){....}
public String requestedUsername(){...}
public String[] availableNames(){...}
}
在新的程式碼中有兩個有用的方法:reqeuestedUsername(),客戶但可以通過它得到請求的名稱;availableNames(),客戶端可以通過它得到一組有用的usernames。這樣客戶端在得到其返回的資訊來明確自己的操作失敗的原因。但是如果你不想新增更多的資訊,那麼你可以丟擲一個標準的Exception:
throw new Exception("Username already taken");
更甚的情況,如果你認為客戶端並不想用過多的操作而僅僅想看到異常資訊,你可以丟擲一個unchecked exception:
throw new RuntimeException("Username already taken");
另外,你可以提供一個方法來驗證該username是否被佔用。
很有必要再重申一下,checked exception應該讓客戶端從中得到豐富的資訊。要想讓你的程式碼更加易讀,請傾向於用unchecked excetpion來處理程式中的錯誤(Prefer unchecked exceptions for all programmatic errors)。
4. Document exceptions.
你可以通過Javadoc's @throws 標籤來說明(document)你的API中要丟擲checked exception或者unchecked exception。然而,我更傾向於使用來單元測試來說明(document)異常。不管你採用哪中方式,你要讓客戶端程式碼知道你的API中所要丟擲的異常。這裡有一個用單元測試來測試IndexOutOfBoundsException的例子:
public void testIndexOutOfBoundsException() {
ArrayList blankList = new ArrayList();
try {
blankList.get(10);
fail("Should raise an IndexOutOfBoundsException");
} catch (IndexOutOfBoundsException success) {}
}
上邊的程式碼在請求blankList.get(10)的時候會丟擲IndexOutOfBoundsException,如果沒有被丟擲,將fail ("Should raise an IndexOutOfBoundsException")顯示說明該測試失敗。通過書寫測試異常的單元測試,你不但可以看到異常是怎樣的工作的,而且你可以讓你的程式碼變得越來越健壯。
下面作者將介紹界中使用異常的最佳實踐(Best Practices for Using Exceptions)
1. 總是要做一些清理工作(Always clean up after yourself)
如果你使用一些資源例如資料庫連線或者網路連線,請記住要做一些清理工作(如關閉資料庫連線或者網路連線),如果你的API丟擲Unchecked exception,那麼你要用try-finally來做必要的清理工作:
- publicvoid dataAccessCode(){
- Connection conn = null;
- try{
- conn = getConnection();
- ..some code that throws SQLException
- }catch(SQLException ex){
- ex.printStacktrace();
- } finally{
- DBUtil.closeConnection(conn);
- }
- }
- class DBUtil{
- publicstaticvoid closeConnection
- (Connection conn){
- try{
- conn.close();
- } catch(SQLException ex){
- logger.error("Cannot close connection");
- thrownew RuntimeException(ex);
- }
- }
- }
DBUtil是一個工具類來關閉Connection.有必要的說的使用的finally的重要性是不管程式是否碰到異常,它都會被執行。在上邊的例子中,finally中關閉連線,如果在關閉連線的時候出現錯誤就丟擲RuntimeException.
2. 不要使用異常來控制流程(Never use exceptions for flow control)
下邊程式碼中,MaximumCountReachedException被用於控制流程:
- publicvoid useExceptionsForFlowControl() {
- try {
- while (true) {
- increaseCount();
- }
- } catch (MaximumCountReachedException ex) {
- }
- //Continue execution
- }
- publicvoid increaseCount()
- throws MaximumCountReachedException {
- if (count >= 5000)
- thrownew MaximumCountReachedException();
- }
上邊的useExceptionsForFlowControl()用一個無限迴圈來增加count直到丟擲異常,這種做法並沒有說讓程式碼不易讀,但是它是程式執行效率降低。
記住,只在要會丟擲異常的地方進行異常處理。
3. 不要忽略異常
當有異常被丟擲的時候,如果你不想恢復它,那麼你要毫不猶豫的將其轉換為unchecked exception,而不是用一個空的catch塊或者什麼也不做來忽略它,以至於從表面來看象是什麼也沒有發生一樣。
4. 不要捕獲頂層的Exception
unchecked exception都是RuntimeException的子類,RuntimeException又繼承Exception,因此,如果單純的捕獲Exception,那麼你同樣也捕獲了RuntimeException,如下程式碼:
try{
..
}catch(Exception ex){
}
一旦你寫出了上邊的程式碼(注意catch塊是空的),它將忽略所有的異常,包括unchecked exception.
5. Log exceptions just once
Logging the same exception stack trace more than once can confuse the programmer examining the stack trace about the original source of exception. So just log it once.