本文是關於 Exception 處理的一篇不錯的文章,從 Java Exception 的概念介紹起,依次講解了 Exception 的類型(Checked/Unchecked),Exception 處理的最佳實現:html
Best Practices for Exception Handling
By Gunjan Doshi 11/19/2003
原文連接: http://www.onjava.com/pub/a/o...
關於異常處理的問題之一就是要知道什麼時候(when)和如何(how)使用它。在本文中我將介紹一些關於異常處理的最佳實踐,同時我也會總結最近關於 checked Exception 使用問題的一些爭論。java
做爲程序員,咱們都但願能寫出解決問題而且是高質量的代碼。不幸的是,異常是伴隨着咱們的代碼產生的反作用(side effects)。沒有人喜歡反作用(side effects),因此咱們很快就找到(find)了咱們本身的方式來避免它,我曾經看到一些聰明的程序員用下面的方式來處理異常:react
public void consumeAndForgetAllExceptions() { try { //...some code that throws exceptions } catch (Exception ex){ ex.printStacktrace(); } }
上邊的代碼有什麼問題麼?
一旦拋出異常,正常的程序執行流程被暫停而且將控制交給catch塊,catch塊捕獲異常而且只是 suppresses it(在控制檯打印出異常信息),以後程序繼續執行,從表面上看就像什麼都沒有發生過同樣……程序員
那下面的這種方式呢?數據庫
public void someMethod() throws Exception { }
他的方法體是空的,它不實現任何的功能(沒有一句代碼),空白方法怎麼(how)會(can)拋出異常?JAVA並不阻止你這麼作。最近,我也遇到相似的代碼,方法聲明中會拋出異常,可是沒有實際發生(generated)該異常的代碼。當我問程序員爲何要這樣作,他回答說「我知道這樣會影響API,但我已經習慣了這樣作並且它頗有效。」編程
C++社區曾經花了數年時間來決定(decide)如何使用異常,關於此類的爭論在 java社區纔剛剛開始。我看到許多Java程序員艱難(struggle)的使用異常。若是沒有正確使用,異常會影響程序的性能,由於它須要使用內存和CPU來建立,拋出以及捕獲異常。若是過度的依賴異常處理,會使得代碼難以閱讀,並使使用API的程序員感到沮喪,咱們都知道這將會帶來代碼漏洞(hacks)和代碼異味(code smells),
客戶端代碼能夠經過忽略異常或拋出異常來避開這個問題,如前兩個示例所示。網絡
從廣義上講,有三種不一樣的情景會致使異常的拋出:app
NullPointerException
和IllegalArgumentException
),客戶端一般沒法對這些編程錯誤採起任何措施。Java 定義了兩類異常:ide
Exception
類繼承的異常都是檢查型異常(checked exceptions),客戶端必須處理API拋出的這類異常,經過catch
子句捕獲或是經過throws
子句繼續拋出(forwarding it outward)。RuntimeException
也是 Exception
的子類,然而,從RuntimeException
繼承的全部異常都會獲得特殊處理。客戶端代碼不須要專門處理這類異常,所以它們被稱爲 Unchecked exceptions.NullPointerException
的繼承關係。NullPointerException
繼承自 RuntimeException
,因此它是 Unchecked exception.我見過大量使用 checked exceptions 只在極少數時候使用 Unchecked exceptions。最近,Java社區關於 checked exceptions 及其真正價值進行了熱烈討論,爭論源於Java彷佛是第一個帶有 checked exceptions 的主流<abbr title="面向對象(Object Oriented)">OO</abbr>語言,而C++和C#根本沒有 checked exception,它們全部的異常都是unchecked .性能
從低層拋出的 checked exception 強制要求調用方捕獲或是拋出該異常。一旦客戶端不能有效地處理這些被拋出的異常,API和客戶端之間的異常協議(checked exception contract)就會變成沒必要要的負擔。客戶端的程序員能夠經過將異常抑制(suppressing)在一個空的catch塊中或是直接拋出它。從而又將這個負擔交給了客戶端的調用者。
Checked exception還被指責可能會破壞封裝,看下面的代碼:
public List getAllAccounts() throws FileNotFoundException, SQLException{ ... }
getAllAccounts()
方法拋出了兩個檢查型異常。調用此方法的客戶端必須明確的處理這兩種具體的異常,即便它並不知道在 getAllAccounts()
中哪一個文件或是數據庫調用失敗了,
或者沒有提供文件系統或數據庫邏輯的業務,所以,這樣的異常處理致使方法和調用者之間不當的強耦合(tight coupling)。
在討論了這些以後,如今讓咱們來探討一下如何設計一個正確拋出異常的API。
若是客戶端能夠採起措施從異常中恢復,那就選擇 checked exception 。若是客戶端不能採起有效的措施,就選擇 unchecked exceptions 。有效的措施是指從異常中恢復的措施,而不只僅是記錄異常日誌。總結一下:
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 的優勢在於不強制客戶端顯示的處理它,它會傳播(propagate)到任何你想捕獲它的地方,或者它會在出現的地方掛起程序並報告異常信息。Java API中提供了豐富的 unchecked excetpion,如:NullPointerException
, IllegalArgumentException
和 IllegalStateException
等。我更傾向於使用JAVA提供的標準異常類而不肯建立新的異常類,這樣使個人代碼易於理解並避免過多的消耗內存。
永遠不要讓特定於實現的 checked exception 傳遞到更高層,好比,不要將數據訪問層的 SQLException
傳遞到業務層,業務層並不須要瞭解(不關心? ) SQLException
,你有兩種方法來解決這種問題:
SQLException
轉換爲另外一個 checked exception 。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
被拋出,catch塊就會拋出一個RuntimeException
,當前執行的線程將會中止並報告該異常。
可是,該異常並無影響到個人業務邏輯模塊,它無需進行異常處理,更況且它根本沒法對SQLException
進行任何操做。若是個人catch塊須要根異常緣由,可使用從JDK1.4開始全部異常類中都有的getCause()
方法。
若是你確信在SQLException
被拋出時業務層能夠執行某些恢復操做,那麼你能夠將其轉換爲一個更有意義的 unchecked exception 。可是我發如今大多時候拋出RuntimeException
已經足夠用了。
如下代碼有什麼問題?
public class DuplicateUsernameException extends Exception {}
它除了有一個「意義明確」(indicative exception)的名字之外,它沒有給客戶端代碼提供任何有用的信息。不要忘記 Exception
跟其餘的Java類同樣,你能夠添加你認爲客戶端代碼將調用的方法供客戶端調用,以得到有用的信息。
咱們能夠爲 DuplicateUsernameException
添加一些必要的方法,以下:
public class DuplicateUsernameException extends Exception { public DuplicateUsernameException (String username){....} public String requestedUsername(){...} public String[] availableNames(){...} }
新版本提供了兩個有用的方法: requestedUsername()
,它會返回請求的名稱。availableNames()
,它會返回一組與請求相似的可用的usernames。客戶端可使用這些方法來告知所請求的用戶名不可用,其餘用戶名可用。可是若是你不許備添加這些額外的信息,那麼只需拋出一個標準的Exception:
throw new Exception("Username already taken");
若是你認爲客戶端代碼除了記錄已經採用的用戶名以外不會進行任何操做,那麼最好拋出 unchecked exception :
throw new RuntimeException("Username already taken");
另外,你能夠提供一個方法來驗證該username是否被佔用。
頗有必要再重申一下,在客戶端API能夠根據異常信息進行某些操做的狀況下,將使用 checked exception 。
處理程序中的錯誤更傾向於用 unchecked excetpion (Prefer unchecked exceptions for all programmatic errors)。它們使你的代碼更具可讀性。
你可使用 Javadoc 的 @throws
標籤來講明(document)你的API中要拋出 checked exception 或者 unchecked exception。然而,我更傾向於使用來單元測試來文檔化異常(document exception)。單元測試容許我在使用中查看異常,而且做爲一個能夠被執行的文檔來使用。無論你採用哪一種方式,你要讓客戶端代碼知道你的API中所要拋出的異常。這是一個用單元測試來測試IndexOutOfBoundsException的例子: 這裏提供了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")
顯式的說明該測試失敗了。經過爲異常編寫單元測試,你不只能夠記錄異常如何觸發,還可使你的代碼在通過這些測試後更加健壯。
下一組最佳實踐展現了客戶端代碼應如何處理拋出 checked exception 的API。
若是你在使用如數據庫鏈接或是網絡鏈接之類的資源,請記住要作一些清理工做 (如關閉數據庫鏈接或者網絡鏈接),若是你調用的API僅拋出 Unchecked exception ,你應該在使用後用try - finally
塊清理資源。
public void dataAccessCode(){ Connection conn = null; try{ conn = getConnection(); //...some code that throws SQLException }catch(SQLException ex){ ex.printStacktrace(); } finally{ DBUtil.closeConnection(conn); } } class DBUtil{ public static void closeConnection (Connection conn){ try{ conn.close(); } catch(SQLException ex){ logger.error("Cannot close connection"); throw new RuntimeException(ex); } } }
DBUtil
類關閉 Connection
鏈接,這裏的重點在於 finally
塊,無論程序是否碰到異常,它都會被執行。在上邊的例子中,在 finally
中關閉鏈接,若是在關閉鏈接的時候出現錯誤就拋出 RuntimeException
。
生成堆棧跟蹤 (stack trace) 的代價很昂貴,堆棧跟蹤的價值在於debug中使用。在一個流程控制中,堆棧跟蹤應當被忽視,由於客戶端只想知道如何進行。
在下面的代碼中,MaximumCountReachedException
被用來進行流程控制:
public void useExceptionsForFlowControl() { try { while (true) { increaseCount(); } } catch (MaximumCountReachedException ex) { } //Continue execution } public void increaseCount() throws MaximumCountReachedException { if (count >= 5000) throw new MaximumCountReachedException(); }
useExceptionsForFlowControl()
用一個無限循環來增長count直到拋出異常,這種方式使得代碼難以閱讀,並且影響代碼性能。只在要會拋出異常的地方進行異常處理。
當API中的方法拋出 checked exception 時,它在提醒你應當採起一些措施。若是 checked exception 沒有任何意義,請絕不猶豫的將其轉化爲 unchecked exception 再從新拋出。而不是用一個空的 catch
塊捕捉來忽略它,而後繼續執行,以致於從表面來看彷彿什麼也沒有發生同樣。
unchecked exception
都是 RuntimeException
的子類,而 RuntimeException
又繼承自 Exception
,若是單純的捕獲 Exception
, 那麼你一樣也捕獲了 RuntimeException
,如如下代碼所示:
try{ // ... }catch(Exception ex){ }
上邊的代碼(注意catch
塊是空的)將忽略全部的異常,包括 unchecked exception
.
將相同的異常屢次記入日誌會使得檢查追蹤棧的開發人員感到困惑,不知道何處是報錯的根源。因此只記錄一次。
These are some suggestions for exception-handling best practices. I have no intention of staring a religious war on checked exceptions vs. unchecked exceptions. You will have to customize the design and usage according to your requirements. I am confident that over time, we will find better ways to code with exceptions.
I would like to thank Bruce Eckel, Joshua Kerievsky, and Somik Raha for their support in writing this article.