異常處理的問題之一是知道什麼時候以及如何去使用它。我會討論一些異常處理的最佳實踐,也會總結最近在異常處理上的一些爭論。程序員
做爲程序員,咱們想要寫高質量的可以解決問題的代碼。可是,異常常常是伴隨着代碼產生的反作用。沒有人喜歡反作用,所以咱們會試圖用本身的方式來解決這個問題。我看過很多的程序用下面的方法應對異常:面試
public void consumeAndForgetAllExceptions(){ try { ...some code that throws exceptions } catch (Exception ex){ ex.printStacktrace(); } }
上面這段代碼的問題在哪裏?數據庫
一旦一個異常被拋出以後,正常的執行流程會中止而且將控制交給捕捉塊。捕捉塊捕獲異常,而後只是把它的信息打印了一下。以後程序正常運行,就像沒有任何事情發生同樣。編程
那下面的這種方法呢?微信
public void someMethod() throws Exception{ }
這是一個空方法,裏面沒有任何的代碼。爲何一個空方法可以拋出異常?JAVA並不阻止你這麼作。最近,我遇到了一些和這個很類似的代碼,明明代碼塊中沒有拋出異常的語句,卻在方法聲明中拋出異常。當我問開發人員爲何這麼作,他會回答「我知道這樣會影響API,可是我以前就這麼作的並且效果還不錯」。網絡
C++社區花了很久才決定如何使用異常。這場爭論也在JAVA社區產生了。我看到很多JAVA開發人員艱難的使用異常。若是不可以正確使用的話,異常會影響程序的性能,由於它須要使用內存和CPU來建立,拋出以及捕獲。若是過分使用的話,會使得代碼難以閱讀,而且影響API的使用人員。咱們都知道這將會帶來代碼漏洞以及壞味道。客戶端代碼常會經過忽略這個異常或是直接將其拋出來避開這個問題,就像以前的兩個例子那樣。框架
從廣義的角度來講,一共有三種不一樣的場景會致使異常的產生:性能
NullPointerException
,IllegalArgumentException
)。客戶端一般沒法對這些錯誤採起任何措施JAVA定義了兩種異常:單元測試
Exception
類繼承的異常都是需檢查異常。客戶端須要處理API拋出的這一類異常,經過try-catch或是繼續拋出。RuntimeException
也是Exception
的子類。可是,繼承了RuntimeException
的類受到了特殊的待遇。客戶端代碼無需專門處理這一類異常。下圖展現了NullPointerException
的繼承樹:測試
上圖中,NullPointerException
繼承自RuntimeException
,所以它也是一個無需檢查的異常。
我看到過大量使用需檢查異常只在極少數時候使用無需檢查異常的。最近,JAVA社區在需檢查異常的真正價值上爆發了熱烈的討論。這場辯論源於JAVA是第一個包含需檢查異常的主流OO框架。C++和C#根本沒有需檢查異常。這些語言中全部的異常都是無需檢查的。
從低層拋出的需檢查異常強制要求調用方捕獲或是拋出該異常。若是客戶端不能有效的處理該異常,API和客戶端之間的異常協議將會帶來極大的負擔。客戶端的開發人員可能會經過將異常抑制在一個空的捕獲塊中或是直接拋出它。從而又將這個負擔交給了客戶端的調用方。
還有人指責需檢查異常會破壞封裝,看下面這段代碼:
public List getAllAccounts() throws FileNotFoundException, SQLException{ ... }
getAllAccounts()
方法拋出了兩個需檢查異常。調用這個方法的客戶端必須明確的處理這兩種具體的異常,即便它們並不清楚getAllAccount()
內到底是哪一個文件訪問或是數據庫訪問失敗了,並且它們也沒有提供文件系統或是數據庫的邏輯。所以,這樣的異常處理致使方法和調用者以前出現了不當的強耦合。
在討論了這些以後,咱們能夠來探討一下如何設計一個正確拋出異常的良好的API。
1.在選擇拋出需肯定異常或是無需肯定異常時,問本身這樣的一個問題:客戶端代碼在遇到異常時會進行怎樣的處理?
若是客戶端可以採起措施從這個異常中恢復過來,那就選擇需肯定異常。若是客戶端不能採起有效的措施,就選擇無需肯定異常。有效的措施是指從異常中恢復的措施,而不只僅是記錄錯誤日誌。
除此之外,儘可能選擇無需肯定的異常:它的優勢在於不會強迫客戶端顯式地處理這種異常。它會冒泡到任何你想捕獲它的地方。JAVA API提供了許多無需檢查的異常如NullPointerException
, IllegalArgumentException
和IllegalStateException
。我傾向於使用JAVA提供的標準的異常,儘可能不去建立本身的異常。
2.保留封裝
永遠不要將特定於實現的異常傳遞到更高層。好比,不要將數據層的SQLException
傳遞出去。業務層不須要了解SQLException
。你有兩個選擇:
SQLException
轉換爲另外一個需檢查異常,若是客戶代碼須要從異常中恢復。SQLException
轉換爲無需檢查異常,若是客戶端代碼沒法對其進行處理。大多數時候,客戶代碼沒法解決SQLException
。這時候就將其轉化爲無需檢查的異常。
public void dataAccessCode(){ try{ ..some code that throws SQLException }catch(SQLException ex){ ex.printStacktrace(); } }
這裏的catch塊並無作任何事情。不如經過以下的方式解決它:
public void dataAccessCode(){ try{ ..some code that throws SQLException }catch(SQLException ex){ throw new RuntimeException(ex); } }
這裏將SQLException
轉化爲了RuntimeException
。若是SQLException
出現了,catch塊就會拋出一個運行時異常。當前執行的線程將會中止並報告該異常。可是,該異常並無影響到個人業務邏輯模塊,它無需進行異常處理,更況且它根本沒法對SQLException
進行任何操做。若是個人catch塊須要根異常緣由,可使用getCause()
方法。
若是你確信業務層能夠採起補救措施,你能夠將其轉化爲一個更有意義的無需檢查異常。可是我以爲拋出RuntimeException足以適用大多數的場景。
3.當沒法提供更加有用信息時,不要自定義異常
下面這段代碼有什麼問題?
public class DuplicateUsernameException extends Exception {}
它沒有給客戶端代碼提供任何有用的信息,除了一個稍微具備含義的命名。不要忘了Exception
類和別的類同樣,在裏面你能夠添加一下方法供客戶端調用,得到有用的信息。
public class DuplicateUsernameException extends Exception { public DuplicateUsernameException (String username){....} public String requestedUsername(){...} public String[] availableNames(){...} }
新版本的異常提供了兩個有用的方法:requestedUsername()
,它會返回請求的名字,和availableNames()
,它會返回一組相近的可用的用戶名。客戶端可使用這些方法來獲取有用的信息。可是若是你不許備添加這些額外的信息,那就拋出一個標準的異常便可。
throw new Exception("Username already taken");
若是你以爲客戶端代碼在記錄日誌以外對這個異常不能進行任何操做,那麼最好拋出無需檢查異常:
throw new RuntimeException("Username already taken");
除此之外,你還能夠提供一個方法來檢查用戶名是否已經被使用。
4.文檔化異常
你可使用Javadoc的@throws
標記來記錄需檢查異常和無需檢查異常。可是,我傾向於寫單元測試來文檔化異常。單元測試容許我在使用中查看異常,而且做爲一個能夠被執行的文檔來使用。不管你採用哪一種方法,儘可能使你的客戶端代碼瞭解你的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");
顯式的說明該測試失敗了。經過爲異常編寫測試,你不只能記錄異常如何觸發,並且使你的代碼在通過這些測試後更加健壯。
1.自覺清理資源
若是你在使用如數據庫鏈接或是網絡鏈接之類的資源,要確保你及時的清理這些資源。若是你調用的API僅僅出發了無需檢查異常,你仍然須要在使用後主動清理。使用try-catch
塊。
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
塊中關閉鏈接,不管是否出現了異常。
2.永遠不要使用異常來控制流
生成棧追蹤的代價很昂貴,它的價值在於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()
經過無限循環來增長計數,直到拋出異常。這種方式使得代碼難以閱讀,並且影響代碼性能。只在出現異常的場景拋出異常。
3.不要無視或是壓制異常
當API的方法會拋出異常的時候,它在提醒你應當採起一些措施。若是需檢查異常沒有任何意義,那就乾脆將其轉化爲無需檢查異常再從新拋出。不要單純的用catch捕獲它而後繼續執行,彷彿什麼都沒有發生同樣。
4.不要捕獲最高層異常
繼承RuntimeException
的異常一樣是Exception
的子類。捕獲Exception
的同時,也捕獲了運行時異常:
try{ .. }catch(Exception ex){ }
5.只記錄異常一次
將同一個異常屢次記入日誌會使得檢查追蹤棧的開發人員感到困惑,不知道何處是報錯的根源。因此只記錄一次。
想要了解更多開發技術,面試教程以及互聯網公司內推,歡迎關注個人微信公衆號!將會不按期的發放福利哦~