摘要:然而,我更傾向于使用來單元測試來文檔化異常。單元測試允許我在使用中查看異常,并且作為一個可以被執行的文檔來使用。通過為異常編寫單元測試,你不僅可以記錄異常如何觸發,還可以使你的代碼在經過這些測試后更加健壯。
本文是關于 Exception 處理的一篇不錯的文章,從 Java Exception 的概念介紹起,依次講解了 Exception 的類型(Checked/Unchecked),Exception 處理的最佳實現:
選擇 Checked 還是 Unchecked 的幾個經典依據
Exception 的封裝問題
如無必要不要創建自己的 Exception
不要用 Exception 來作流程控制
不要輕易的忽略捕獲的 Exception
不要簡單地捕獲頂層的 Exception
Best Practices for Exception Handling
By Gunjan Doshi 11/19/2003
原文鏈接:http://www.onjava.com/pub/a/o...
關于異常處理的問題之一就是要知道何時(when)和如何(how)使用它。在本文中我將介紹一些關于異常處理的最佳實踐,同時我也會總結最近關于 checked Exception 使用問題的一些爭論。
作為程序員,我們都希望能寫出解決問題并且是高質量的代碼。不幸的是,異常是伴隨著我們的代碼產生的副作用(side effects)。沒有人喜歡副作用(side effects),所以我們很快就找到(find)了我們自己的方式來避免它,我曾經看到一些聰明的程序員用下面的方式來處理異常:
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),
客戶端代碼可以通過忽略異常或拋出異常來避開這個問題,如前兩個示例所示。
從廣義上講,有三種不同的情景會導致異常的拋出:
編程錯誤導致的異常 (Exception due Programming errors):這一類的異常是因為編程錯誤發生的,(如NullPointerException和IllegalArgumentException),客戶端通常無法對這些編程錯誤采取任何措施。
客戶端代碼錯誤導致異常(Exceptions due to client code errors):客戶端代碼試圖調用API不允許的操作,從而違反了合約。如果異常中提供了有用的信息,客戶端可以通過其采用一些替代方法。例如:當解析格式不正確的XML文件時會拋出異常。該異常中包含導致問題發生的XML內容的具體位置。客戶端可以通過這些信息采取恢復措施。
資源失效導致的異常(Exceptions due to resource failures):當資源失效時發生的異常。如內存不足或網絡連接失敗。客戶端對資源失效的回應是要根據上下文來決定的。客戶端可以在一段時間之后重試該操作,或是只記錄資源失效日志并停止應用程序。
Java 異常類型Java 定義了兩類異常:
檢查型異常 (Checked exceptions):從 Exception 類繼承的異常都是檢查型異常(checked exceptions),客戶端必須處理API拋出的這類異常,通過catch子句捕獲或是通過throws子句繼續拋出(forwarding it outward)。
非檢查型異常 (Unchecked exceptions):RuntimeException 也是 Exception 的子類,然而,從RuntimeException 繼承的所有異常都會得到特殊處理。客戶端代碼不需要專門處理這類異常,因此它們被稱為 Unchecked exceptions.
舉例來說,下圖為 NullPointerException 的繼承關系。
圖中,NullPointerException 繼承自 RuntimeException,所以它是 Unchecked exception.
我見過大量使用 checked exceptions 只在極少數時候使用 Unchecked exceptions。最近,Java社區關于 checked exceptions 及其真正價值進行了熱烈討論,爭論源于Java似乎是第一個帶有 checked exceptions 的主流OO語言,而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。
1. 當要決定是采用 checked exceptions 還是 unchecked exceptions 的時候,問自己這樣的一個問題,“如果這種異常一旦拋出,客戶端會進行怎樣的處理?”如果客戶端可以采取措施從異常中恢復,那就選擇 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提供的標準異常類而不愿創建新的異常類,這樣使我的代碼易于理解并避免過多的消耗內存。
2. 保護封裝性 (Preserve encapsulation)永遠不要讓特定于實現的 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") 顯式的說明該測試失敗了。通過為異常編寫單元測試,你不僅可以記錄異常如何觸發,還可以使你的代碼在經過這些測試后更加健壯。
使用異常的最佳實踐 (Best Practices for Using Exceptions)下一組最佳實踐展示了客戶端代碼應如何處理拋出 checked exception 的API。
1. 總是要做一些清理工作 (Always clean up after yourself)如果你在使用如數據庫連接或是網絡連接之類的資源,請記住要做一些清理工作 (如關閉數據庫連接或者網絡連接),如果你調用的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 。
2. 不要使用異常來控制流程 (Never use exceptions for flow control)生成堆棧跟蹤 (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直到拋出異常,這種方式使得代碼難以閱讀,而且影響代碼性能。只在要會拋出異常的地方進行異常處理。
3. 不要忽略異常 (Do not suppress or ignore exceptions)當API中的方法拋出 checked exception 時,它在提醒你應當采取一些措施。如果 checked exception 沒有任何意義,請毫不猶豫的將其轉化為 unchecked exception 再重新拋出。而不是用一個空的 catch 塊捕捉來忽略它,然后繼續執行,以至于從表面來看仿佛什么也沒有發生一樣。
4. 不要捕獲頂層的Exception (Do not catch top-level exceptions)unchecked exception 都是 RuntimeException 的子類,而 RuntimeException 又繼承自 Exception,如果單純的捕獲 Exception , 那么你同樣也捕獲了 RuntimeException ,如以下代碼所示:
try{ // ... }catch(Exception ex){ }
上邊的代碼(注意catch塊是空的)將忽略所有的異常,包括 unchecked exception .
5. 只記錄異常一次 (Log exceptions just once)將相同的異常多次記入日志會使得檢查追蹤棧的開發人員感到困惑,不知道何處是報錯的根源。所以只記錄一次。
SummaryThese 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.
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/72621.html
摘要:為可恢復的錯誤使用檢查型異常,為編程錯誤使用非檢查型錯誤。檢查型異常保證你對錯誤條件提供異常處理代碼,這是一種從語言到強制你編寫健壯的代碼的一種方式,但同時會引入大量雜亂的代碼并導致其不可讀。在編程中選擇檢查型異常還是運行時異常。 異常處理是Java 開發中的一個重要部分。它是關乎每個應用的一個非功能性需求,是為了處理任何錯誤狀況,比如資源不可訪問,非法輸入,空輸入等等。Java提供了...
摘要:異常處理的個最佳實踐原文地址翻譯出處在中,異常處理是個很麻煩的事情。使用描述性消息拋出異常這個最佳實踐背后的想法與前兩個類似。當你以錯誤的格式提供時,它將被類的構造函數拋出。類提供了特殊的構造函數方法,它接受一個作為參數。 Java 異常處理的 9 個最佳實踐 原文地址:https://dzone.com/articles/9-...翻譯出處:https://www.oschina.n...
摘要:無需檢查的異常也是的子類。從低層拋出的需檢查異常強制要求調用方捕獲或是拋出該異常。當前執行的線程將會停止并報告該異常。單元測試允許我在使用中查看異常,并且作為一個可以被執行的文檔來使用。不要捕獲最高層異常繼承的異常同樣是的子類。 前言 異常處理的問題之一是知道何時以及如何去使用它。我會討論一些異常處理的最佳實踐,也會總結最近在異常處理上的一些爭論。 作為程序員,我們想要寫高質量的能夠解...
摘要:不相等的對象要具有不相等的哈希碼為了哈希表的操作效率,這一點很重要,但不是強制要求,最低要求是不相等的對象不能共用一個哈希碼。方法和方法協同工作,返回對象的哈希碼。這個哈希碼基于對象的身份生成,而不是對象的相等性。 本文面向 剛學完Java的新手們。這篇文章不講語法,而是一些除了語法必須了解的概念。 將要去面試的初級工程師們。查漏補缺,以免遭遇不測。 目前由于篇幅而被挪出本文的知識...
摘要:關于異常處理的文章已有相當的篇幅,本文簡單總結了的異常處理機制,并結合代碼分析了一些異常處理的最佳實踐,對異常的性能開銷進行了簡單分析。是程序正常運行中,可以預料的意外情況,應該被捕獲并進行相應處理。 關于異常處理的文章已有相當的篇幅,本文簡單總結了Java的異常處理機制,并結合代碼分析了一些異常處理的最佳實踐,對異常的性能開銷進行了簡單分析。博客另一篇文章《[譯]Java異常處理的最...
閱讀 2576·2021-10-25 09:45
閱讀 1238·2021-10-14 09:43
閱讀 2297·2021-09-22 15:23
閱讀 1519·2021-09-22 14:58
閱讀 1933·2019-08-30 15:54
閱讀 3538·2019-08-30 13:00
閱讀 1354·2019-08-29 18:44
閱讀 1570·2019-08-29 16:59