摘要:無需檢查的異常也是的子類。從低層拋出的需檢查異常強制要求調用方捕獲或是拋出該異常。當前執行的線程將會停止并報告該異常。單元測試允許我在使用中查看異常,并且作為一個可以被執行的文檔來使用。不要捕獲最高層異常繼承的異常同樣是的子類。
前言
異常處理的問題之一是知道何時以及如何去使用它。我會討論一些異常處理的最佳實踐,也會總結最近在異常處理上的一些爭論。
作為程序員,我們想要寫高質量的能夠解決問題的代碼。但是,異常經常是伴隨著代碼產生的副作用。沒有人喜歡副作用,因此我們會試圖用自己的方式來解決這個問題。我看過不少的程序用下面的方法應對異常:
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的使用人員。我們都知道這將會帶來代碼漏洞以及壞味道??蛻舳舜a常會通過忽略這個異常或是直接將其拋出來避開這個問題,就像之前的兩個例子那樣。
異常的本質從廣義的角度來說,一共有三種不同的場景會導致異常的產生:
編程錯誤導致的異常:這一類的異常是因為不恰當的編程帶來的(比如NullPointerException,IllegalArgumentException)。客戶端通常無法對這些錯誤采取任何措施
客戶端代碼的錯誤:客戶端代碼在API允許的范圍之外使用API,從而違背了合約??蛻舳丝梢酝ㄟ^異常中提供的有用信息,采用一些替代方法。比如,當解析格式不正確的XML文件時,會拋出異常。這個異常中包含導致該錯誤發生的XML內容的具體位置。客戶端可以通過這些信息采取回復措施。
資源失效導致的異常:比如系統內存不足或是網絡連接失敗??蛻舳嗣鎸Y源失效的回應是要根據上下文來決定的??蛻舳丝梢栽谝欢螘r間之后試著重新連接或是記錄資源失效日志然后暫停應用程序。
JAVA異常類型JAVA定義了兩種異常:
需檢查的異常:從Exception類繼承的異常都是需檢查異常??蛻舳诵枰幚鞟PI拋出的這一類異常,通過try-catch或是繼續拋出。
無需檢查的異常:RuntimeException也是Exception的子類。但是,繼承了RuntimeException的類受到了特殊的待遇??蛻舳舜a無需專門處理這一類異常。
下圖展示了NullPointerException的繼承樹:
上圖中,NullPointerException繼承自RuntimeException,因此它也是一個無需檢查的異常。
我看到過大量使用需檢查異常只在極少數時候使用無需檢查異常的。最近,JAVA社區在需檢查異常的真正價值上爆發了熱烈的討論。這場辯論源于JAVA是第一個包含需檢查異常的主流OO框架。C++和C#根本沒有需檢查異常。這些語言中所有的異常都是無需檢查的。
從低層拋出的需檢查異常強制要求調用方捕獲或是拋出該異常。如果客戶端不能有效的處理該異常,API和客戶端之間的異常協議將會帶來極大的負擔??蛻舳说拈_發人員可能會通過將異常抑制在一個空的捕獲塊中或是直接拋出它。從而又將這個負擔交給了客戶端的調用方。
還有人指責需檢查異常會破壞封裝,看下面這段代碼:
public List getAllAccounts() throws FileNotFoundException, SQLException{ ... }
getAllAccounts()方法拋出了兩個需檢查異常。調用這個方法的客戶端必須明確的處理這兩種具體的異常,即使它們并不清楚getAllAccount()內究竟是哪個文件訪問或是數據庫訪問失敗了,而且它們也沒有提供文件系統或是數據庫的邏輯。因此,這樣的異常處理導致方法和調用者之前出現了不當的強耦合。
設計API的最佳實踐在討論了這些之后,我們可以來探討一下如何設計一個正確拋出異常的良好的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.只記錄異常一次
將同一個異常多次記入日志會使得檢查追蹤棧的開發人員感到困惑,不知道何處是報錯的根源。所以只記錄一次。
想要了解更多開發技術,面試教程以及互聯網公司內推,歡迎關注我的微信公眾號!將會不定期的發放福利哦~
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/68595.html
摘要:是指可能導致程序終止的非常嚴重的時間。具有最高的級別,旨在關閉中的日志功能。因此為每一個消息選擇一個合適的日志級別是非常重要的。日志的個小建議將日志訪日代碼塊它能顯著的減少因為字符串拼接而帶來的性能的影響。 前言 首先,這篇文章沒有進行任何的日志功能的詳細介紹,而是對日志提出了幾種最佳實踐。適合對日志記錄有所了解的同學閱讀。下面是正文: JAVA日志管理既是一門科學,又是一門藝術??茖W...
摘要:我們會寫切面來攔截對這些業務類和類的調用。切面定義何時攔截一個方法以及做什么和在一起成為切面連接點當代碼開始執行,并且切點的條件滿足時,通知被調用。 前言 這篇文章會幫助你使用Spring Boot Starter AOP實現AOP。我們會使用AspectJ實現四個不同的通知(advice),并且新建一個自定義的注解來追蹤方法的執行時間。 你將會了解 什么是交叉分割關注點(cross...
摘要:這個例子想要說明兩個事情中以為結尾的方法將會異步執行默認情況下即指沒有傳入的情況下,異步執行會使用實現,該線程池使用一個后臺線程來執行任務。這個例子展示了如何使用一個固定大小的線程池來實現大寫操作。 前言 這篇博客回顧JAVA8的CompletionStageAPI以及其在JAVA庫中的標準實現CompletableFuture。將會通過幾個例子來展示API的各種行為。 因為Compl...
摘要:前言上一篇文章請參考貓頭鷹的深夜翻譯核心并發一安全發布發布一個對象是指該對象的引用對當前的域之外也可見比如,從方法中獲取一個引用。任務的功能性接口表示一個沒有返回值的任務表示一個包含返回值的計算。 前言 上一篇文章請參考貓頭鷹的深夜翻譯:核心JAVA并發(一) 安全發布 發布一個對象是指該對象的引用對當前的域之外也可見(比如,從getter方法中獲取一個引用)。要確保一個對象被安全的發...
摘要:由于需要跨進程訪問網絡上的高速緩存,因此延遲,故障和對象序列化會導致性能下降。應用程序高速緩存會自動清除條目以保持其內存占用。緩存統計高速緩存統計信息可幫助識別高速緩存的運行狀況并提供有關高速緩存行為和性能的信息。 前言 這篇文章探索了現有的各種JAVA緩存基數,它們對各種場景下提高應用的性能起著重要的作用。 近十年來,信息技術極高的提升了業務流程,它已經成為了全球企業的戰略性方案。它...
閱讀 3325·2021-11-19 11:36
閱讀 2926·2021-09-27 13:34
閱讀 1989·2021-09-22 15:17
閱讀 2403·2019-08-30 13:49
閱讀 753·2019-08-26 13:58
閱讀 1357·2019-08-26 10:47
閱讀 2536·2019-08-23 18:05
閱讀 598·2019-08-23 14:25