摘要:然而,我更傾向于使用來單元測試來文檔化異常。單元測試允許我在使用中查看異常,并且作為一個(gè)可以被執(zhí)行的文檔來使用。通過為異常編寫單元測試,你不僅可以記錄異常如何觸發(fā),還可以使你的代碼在經(jīng)過這些測試后更加健壯。
本文是關(guān)于 Exception 處理的一篇不錯(cuò)的文章,從 Java Exception 的概念介紹起,依次講解了 Exception 的類型(Checked/Unchecked),Exception 處理的最佳實(shí)現(xiàn):
選擇 Checked 還是 Unchecked 的幾個(gè)經(jīng)典依據(jù)
Exception 的封裝問題
如無必要不要?jiǎng)?chuàng)建自己的 Exception
不要用 Exception 來作流程控制
不要輕易的忽略捕獲的 Exception
不要簡單地捕獲頂層的 Exception
Best Practices for Exception Handling
By Gunjan Doshi 11/19/2003
原文鏈接:http://www.onjava.com/pub/a/o...
關(guān)于異常處理的問題之一就是要知道何時(shí)(when)和如何(how)使用它。在本文中我將介紹一些關(guān)于異常處理的最佳實(shí)踐,同時(shí)我也會(huì)總結(jié)最近關(guān)于 checked Exception 使用問題的一些爭論。
作為程序員,我們都希望能寫出解決問題并且是高質(zhì)量的代碼。不幸的是,異常是伴隨著我們的代碼產(chǎn)生的副作用(side effects)。沒有人喜歡副作用(side effects),所以我們很快就找到(find)了我們自己的方式來避免它,我曾經(jīng)看到一些聰明的程序員用下面的方式來處理異常:
public void consumeAndForgetAllExceptions() { try { //...some code that throws exceptions } catch (Exception ex){ ex.printStacktrace(); } }
上邊的代碼有什么問題么?
一旦拋出異常,正常的程序執(zhí)行流程被暫停并且將控制交給catch塊,catch塊捕獲異常并且只是 suppresses it(在控制臺(tái)打印出異常信息),之后程序繼續(xù)執(zhí)行,從表面上看就像什么都沒有發(fā)生過一樣……
那下面的這種方式呢?
public void someMethod() throws Exception { }
他的方法體是空的,它不實(shí)現(xiàn)任何的功能(沒有一句代碼),空白方法怎么(how)會(huì)(can)拋出異常?JAVA并不阻止你這么做。最近,我也遇到類似的代碼,方法聲明中會(huì)拋出異常,但是沒有實(shí)際發(fā)生(generated)該異常的代碼。當(dāng)我問程序員為什么要這樣做,他回答說“我知道這樣會(huì)影響API,但我已經(jīng)習(xí)慣了這樣做而且它很有效?!?/p>
C++社區(qū)曾經(jīng)花了數(shù)年時(shí)間來決定(decide)如何使用異常,關(guān)于此類的爭論在 java社區(qū)才剛剛開始。我看到許多Java程序員艱難(struggle)的使用異常。如果沒有正確使用,異常會(huì)影響程序的性能,因?yàn)樗枰褂脙?nèi)存和CPU來創(chuàng)建,拋出以及捕獲異常。如果過分的依賴異常處理,會(huì)使得代碼難以閱讀,并使使用API的程序員感到沮喪,我們都知道這將會(huì)帶來代碼漏洞(hacks)和代碼異味(code smells),
客戶端代碼可以通過忽略異?;驋伋霎惓肀荛_這個(gè)問題,如前兩個(gè)示例所示。
從廣義上講,有三種不同的情景會(huì)導(dǎo)致異常的拋出:
編程錯(cuò)誤導(dǎo)致的異常 (Exception due Programming errors):這一類的異常是因?yàn)榫幊体e(cuò)誤發(fā)生的,(如NullPointerException和IllegalArgumentException),客戶端通常無法對這些編程錯(cuò)誤采取任何措施。
客戶端代碼錯(cuò)誤導(dǎo)致異常(Exceptions due to client code errors):客戶端代碼試圖調(diào)用API不允許的操作,從而違反了合約。如果異常中提供了有用的信息,客戶端可以通過其采用一些替代方法。例如:當(dāng)解析格式不正確的XML文件時(shí)會(huì)拋出異常。該異常中包含導(dǎo)致問題發(fā)生的XML內(nèi)容的具體位置。客戶端可以通過這些信息采取恢復(fù)措施。
資源失效導(dǎo)致的異常(Exceptions due to resource failures):當(dāng)資源失效時(shí)發(fā)生的異常。如內(nèi)存不足或網(wǎng)絡(luò)連接失敗??蛻舳藢Y源失效的回應(yīng)是要根據(jù)上下文來決定的??蛻舳丝梢栽谝欢螘r(shí)間之后重試該操作,或是只記錄資源失效日志并停止應(yīng)用程序。
Java 異常類型Java 定義了兩類異常:
檢查型異常 (Checked exceptions):從 Exception 類繼承的異常都是檢查型異常(checked exceptions),客戶端必須處理API拋出的這類異常,通過catch子句捕獲或是通過throws子句繼續(xù)拋出(forwarding it outward)。
非檢查型異常 (Unchecked exceptions):RuntimeException 也是 Exception 的子類,然而,從RuntimeException 繼承的所有異常都會(huì)得到特殊處理。客戶端代碼不需要專門處理這類異常,因此它們被稱為 Unchecked exceptions.
舉例來說,下圖為 NullPointerException 的繼承關(guān)系。
圖中,NullPointerException 繼承自 RuntimeException,所以它是 Unchecked exception.
我見過大量使用 checked exceptions 只在極少數(shù)時(shí)候使用 Unchecked exceptions。最近,Java社區(qū)關(guān)于 checked exceptions 及其真正價(jià)值進(jìn)行了熱烈討論,爭論源于Java似乎是第一個(gè)帶有 checked exceptions 的主流OO語言,而C++和C#根本沒有 checked exception,它們所有的異常都是unchecked .
從低層拋出的 checked exception 強(qiáng)制要求調(diào)用方捕獲或是拋出該異常。一旦客戶端不能有效地處理這些被拋出的異常,API和客戶端之間的異常協(xié)議(checked exception contract)就會(huì)變成不必要的負(fù)擔(dān)??蛻舳说某绦騿T可以通過將異常抑制(suppressing)在一個(gè)空的catch塊中或是直接拋出它。從而又將這個(gè)負(fù)擔(dān)交給了客戶端的調(diào)用者。
Checked exception還被指責(zé)可能會(huì)破壞封裝,看下面的代碼:
public List getAllAccounts() throws FileNotFoundException, SQLException{ ... }
getAllAccounts() 方法拋出了兩個(gè)檢查型異常。調(diào)用此方法的客戶端必須明確的處理這兩種具體的異常,即使它并不知道在 getAllAccounts() 中哪個(gè)文件或是數(shù)據(jù)庫調(diào)用失敗了,
或者沒有提供文件系統(tǒng)或數(shù)據(jù)庫邏輯的業(yè)務(wù),因此,這樣的異常處理導(dǎo)致方法和調(diào)用者之間不當(dāng)?shù)膹?qiáng)耦合(tight coupling)。
在討論了這些之后,現(xiàn)在讓我們來探討一下如何設(shè)計(jì)一個(gè)正確拋出異常的API。
1. 當(dāng)要決定是采用 checked exceptions 還是 unchecked exceptions 的時(shí)候,問自己這樣的一個(gè)問題,“如果這種異常一旦拋出,客戶端會(huì)進(jìn)行怎樣的處理?”如果客戶端可以采取措施從異常中恢復(fù),那就選擇 checked exception 。如果客戶端不能采取有效的措施,就選擇 unchecked exceptions 。有效的措施是指從異常中恢復(fù)的措施,而不僅僅是記錄異常日志??偨Y(jié)一下:
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 來處理編程錯(cuò)誤:unchecked exception 的優(yōu)點(diǎn)在于不強(qiáng)制客戶端顯示的處理它,它會(huì)傳播(propagate)到任何你想捕獲它的地方,或者它會(huì)在出現(xiàn)的地方掛起程序并報(bào)告異常信息。Java API中提供了豐富的 unchecked excetpion,如:NullPointerException , IllegalArgumentException 和 IllegalStateException 等。我更傾向于使用JAVA提供的標(biāo)準(zhǔn)異常類而不愿創(chuàng)建新的異常類,這樣使我的代碼易于理解并避免過多的消耗內(nèi)存。
2. 保護(hù)封裝性 (Preserve encapsulation)永遠(yuǎn)不要讓特定于實(shí)現(xiàn)的 checked exception 傳遞到更高層,比如,不要將數(shù)據(jù)訪問層的 SQLException 傳遞到業(yè)務(wù)層,業(yè)務(wù)層并不需要了解(不關(guān)心? ) SQLException ,你有兩種方法來解決這種問題:
如果需要客戶端代碼從異常中恢復(fù),則將 SQLException 轉(zhuǎn)換為另一個(gè) checked exception 。
如果客戶端代碼無法對其進(jìn)行處理,請將 SQLException 轉(zhuǎn)換為 unchecked exception 。
大多數(shù)情況下,客戶端代碼都是對 SQLException 無能為力的,不要猶豫,把它轉(zhuǎn)換為一個(gè) unchecked exception ,考慮以下代碼:
public void dataAccessCode(){ try{ //...some code that throws SQLException }catch(SQLException ex){ ex.printStacktrace(); } }
這里的catch塊僅僅打印異常信息而沒有任何的直接操作,這樣做的理由是客戶端無法處理 SQLException (但是顯然這種就象什么事情都沒發(fā)生一樣的做法是不可取的),不如通過如下的方式解決它:
public void dataAccessCode(){ try{ //...some code that throws SQLException }catch(SQLException ex){ throw new RuntimeException(ex); } }
這里將 SQLException 轉(zhuǎn)化為了 RuntimeException,一旦SQLException被拋出,catch塊就會(huì)拋出一個(gè)RuntimeException,當(dāng)前執(zhí)行的線程將會(huì)停止并報(bào)告該異常。
但是,該異常并沒有影響到我的業(yè)務(wù)邏輯模塊,它無需進(jìn)行異常處理,更何況它根本無法對SQLException進(jìn)行任何操作。如果我的catch塊需要根異常原因,可以使用從JDK1.4開始所有異常類中都有的getCause()方法。
如果你確信在SQLException被拋出時(shí)業(yè)務(wù)層可以執(zhí)行某些恢復(fù)操作,那么你可以將其轉(zhuǎn)換為一個(gè)更有意義的 unchecked exception 。但是我發(fā)現(xiàn)在大多時(shí)候拋出RuntimeException已經(jīng)足夠用了。
以下代碼有什么問題?
public class DuplicateUsernameException extends Exception {}
它除了有一個(gè)“意義明確”(indicative exception)的名字以外,它沒有給客戶端代碼提供任何有用的信息。不要忘記 Exception 跟其他的Java類一樣,你可以添加你認(rèn)為客戶端代碼將調(diào)用的方法供客戶端調(diào)用,以獲得有用的信息。
我們可以為 DuplicateUsernameException 添加一些必要的方法,如下:
public class DuplicateUsernameException extends Exception { public DuplicateUsernameException (String username){....} public String requestedUsername(){...} public String[] availableNames(){...} }
新版本提供了兩個(gè)有用的方法: requestedUsername(),它會(huì)返回請求的名稱。availableNames(),它會(huì)返回一組與請求類似的可用的usernames??蛻舳丝梢允褂眠@些方法來告知所請求的用戶名不可用,其他用戶名可用。但是如果你不準(zhǔn)備添加這些額外的信息,那么只需拋出一個(gè)標(biāo)準(zhǔn)的Exception:
throw new Exception("Username already taken");
如果你認(rèn)為客戶端代碼除了記錄已經(jīng)采用的用戶名之外不會(huì)進(jìn)行任何操作,那么最好拋出 unchecked exception :
throw new RuntimeException("Username already taken");
另外,你可以提供一個(gè)方法來驗(yàn)證該username是否被占用。
很有必要再重申一下,在客戶端API可以根據(jù)異常信息進(jìn)行某些操作的情況下,將使用 checked exception 。
處理程序中的錯(cuò)誤更傾向于用 unchecked excetpion (Prefer unchecked exceptions for all programmatic errors)。它們使你的代碼更具可讀性。
你可以使用 Javadoc 的 @throws 標(biāo)簽來說明(document)你的API中要拋出 checked exception 或者 unchecked exception。然而,我更傾向于使用來單元測試來文檔化異常(document exception)。單元測試允許我在使用中查看異常,并且作為一個(gè)可以被執(zhí)行的文檔來使用。不管你采用哪種方式,你要讓客戶端代碼知道你的API中所要拋出的異常。這是一個(gè)用單元測試來測試IndexOutOfBoundsException的例子: 這里提供了IndexOutOfBoundsException的單元測試。
public void testIndexOutOfBoundsException() { ArrayList blankList = new ArrayList(); try { blankList.get(10); fail("Should raise an IndexOutOfBoundsException"); } catch (IndexOutOfBoundsException success) {} }
上面這段代碼在調(diào)用 blankList.get(10) 應(yīng)當(dāng)拋出 IndexOutOfBoundsException 。如果沒有拋出該異常,則會(huì)執(zhí)行 fail("Should raise an IndexOutOfBoundsException") 顯式的說明該測試失敗了。通過為異常編寫單元測試,你不僅可以記錄異常如何觸發(fā),還可以使你的代碼在經(jīng)過這些測試后更加健壯。
使用異常的最佳實(shí)踐 (Best Practices for Using Exceptions)下一組最佳實(shí)踐展示了客戶端代碼應(yīng)如何處理拋出 checked exception 的API。
1. 總是要做一些清理工作 (Always clean up after yourself)如果你在使用如數(shù)據(jù)庫連接或是網(wǎng)絡(luò)連接之類的資源,請記住要做一些清理工作 (如關(guān)閉數(shù)據(jù)庫連接或者網(wǎng)絡(luò)連接),如果你調(diào)用的API僅拋出 Unchecked exception ,你應(yīng)該在使用后用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 類關(guān)閉 Connection 連接,這里的重點(diǎn)在于 finally 塊,不管程序是否碰到異常,它都會(huì)被執(zhí)行。在上邊的例子中,在 finally 中關(guān)閉連接,如果在關(guān)閉連接的時(shí)候出現(xiàn)錯(cuò)誤就拋出 RuntimeException 。
2. 不要使用異常來控制流程 (Never use exceptions for flow control)生成堆棧跟蹤 (stack trace) 的代價(jià)很昂貴,堆棧跟蹤的價(jià)值在于debug中使用。在一個(gè)流程控制中,堆棧跟蹤應(yīng)當(dāng)被忽視,因?yàn)榭蛻舳酥幌胫廊绾芜M(jìn)行。
在下面的代碼中,MaximumCountReachedException 被用來進(jìn)行流程控制:
public void useExceptionsForFlowControl() { try { while (true) { increaseCount(); } } catch (MaximumCountReachedException ex) { } //Continue execution } public void increaseCount() throws MaximumCountReachedException { if (count >= 5000) throw new MaximumCountReachedException(); }
useExceptionsForFlowControl() 用一個(gè)無限循環(huán)來增加count直到拋出異常,這種方式使得代碼難以閱讀,而且影響代碼性能。只在要會(huì)拋出異常的地方進(jìn)行異常處理。
3. 不要忽略異常 (Do not suppress or ignore exceptions)當(dāng)API中的方法拋出 checked exception 時(shí),它在提醒你應(yīng)當(dāng)采取一些措施。如果 checked exception 沒有任何意義,請毫不猶豫的將其轉(zhuǎn)化為 unchecked exception 再重新拋出。而不是用一個(gè)空的 catch 塊捕捉來忽略它,然后繼續(xù)執(zhí)行,以至于從表面來看仿佛什么也沒有發(fā)生一樣。
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)將相同的異常多次記入日志會(huì)使得檢查追蹤棧的開發(fā)人員感到困惑,不知道何處是報(bào)錯(cuò)的根源。所以只記錄一次。
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.
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://www.ezyhdfw.cn/yun/72621.html
摘要:為可恢復(fù)的錯(cuò)誤使用檢查型異常,為編程錯(cuò)誤使用非檢查型錯(cuò)誤。檢查型異常保證你對錯(cuò)誤條件提供異常處理代碼,這是一種從語言到強(qiáng)制你編寫健壯的代碼的一種方式,但同時(shí)會(huì)引入大量雜亂的代碼并導(dǎo)致其不可讀。在編程中選擇檢查型異常還是運(yùn)行時(shí)異常。 異常處理是Java 開發(fā)中的一個(gè)重要部分。它是關(guān)乎每個(gè)應(yīng)用的一個(gè)非功能性需求,是為了處理任何錯(cuò)誤狀況,比如資源不可訪問,非法輸入,空輸入等等。Java提供了...
摘要:異常處理的個(gè)最佳實(shí)踐原文地址翻譯出處在中,異常處理是個(gè)很麻煩的事情。使用描述性消息拋出異常這個(gè)最佳實(shí)踐背后的想法與前兩個(gè)類似。當(dāng)你以錯(cuò)誤的格式提供時(shí),它將被類的構(gòu)造函數(shù)拋出。類提供了特殊的構(gòu)造函數(shù)方法,它接受一個(gè)作為參數(shù)。 Java 異常處理的 9 個(gè)最佳實(shí)踐 原文地址:https://dzone.com/articles/9-...翻譯出處:https://www.oschina.n...
摘要:無需檢查的異常也是的子類。從低層拋出的需檢查異常強(qiáng)制要求調(diào)用方捕獲或是拋出該異常。當(dāng)前執(zhí)行的線程將會(huì)停止并報(bào)告該異常。單元測試允許我在使用中查看異常,并且作為一個(gè)可以被執(zhí)行的文檔來使用。不要捕獲最高層異常繼承的異常同樣是的子類。 前言 異常處理的問題之一是知道何時(shí)以及如何去使用它。我會(huì)討論一些異常處理的最佳實(shí)踐,也會(huì)總結(jié)最近在異常處理上的一些爭論。 作為程序員,我們想要寫高質(zhì)量的能夠解...
摘要:不相等的對象要具有不相等的哈希碼為了哈希表的操作效率,這一點(diǎn)很重要,但不是強(qiáng)制要求,最低要求是不相等的對象不能共用一個(gè)哈希碼。方法和方法協(xié)同工作,返回對象的哈希碼。這個(gè)哈希碼基于對象的身份生成,而不是對象的相等性。 本文面向 剛學(xué)完Java的新手們。這篇文章不講語法,而是一些除了語法必須了解的概念。 將要去面試的初級工程師們。查漏補(bǔ)缺,以免遭遇不測。 目前由于篇幅而被挪出本文的知識...
摘要:關(guān)于異常處理的文章已有相當(dāng)?shù)钠?,本文簡單總結(jié)了的異常處理機(jī)制,并結(jié)合代碼分析了一些異常處理的最佳實(shí)踐,對異常的性能開銷進(jìn)行了簡單分析。是程序正常運(yùn)行中,可以預(yù)料的意外情況,應(yīng)該被捕獲并進(jìn)行相應(yīng)處理。 關(guān)于異常處理的文章已有相當(dāng)?shù)钠?,本文簡單總結(jié)了Java的異常處理機(jī)制,并結(jié)合代碼分析了一些異常處理的最佳實(shí)踐,對異常的性能開銷進(jìn)行了簡單分析。博客另一篇文章《[譯]Java異常處理的最...
閱讀 2673·2021-10-25 09:45
閱讀 1310·2021-10-14 09:43
閱讀 2367·2021-09-22 15:23
閱讀 1623·2021-09-22 14:58
閱讀 1994·2019-08-30 15:54
閱讀 3599·2019-08-30 13:00
閱讀 1426·2019-08-29 18:44
閱讀 1633·2019-08-29 16:59