明輝手游網(wǎng)中心:是一個(gè)免費(fèi)提供流行視頻軟件教程、在線學(xué)習(xí)分享的學(xué)習(xí)平臺(tái)!

Java 理論與實(shí)戰(zhàn): 對(duì)于異常的爭(zhēng)論

[摘要]關(guān)于在 Java 語(yǔ)言中使用異常的大多數(shù)建議都認(rèn)為,在確信異常可以被捕獲的任何情況下,應(yīng)該優(yōu)先使用檢查型異常。語(yǔ)言設(shè)計(jì)(編譯器強(qiáng)制您在方法簽名中列出可能被拋出的所有檢查型異常)以及早期關(guān)于樣式和用法的著作都支持該建議。最近,幾位著名的作者已經(jīng)開(kāi)始認(rèn)為非檢查型異常在優(yōu)秀的 Java 類設(shè)計(jì)中有著比以...
關(guān)于在 Java 語(yǔ)言中使用異常的大多數(shù)建議都認(rèn)為,在確信異常可以被捕獲的任何情況下,應(yīng)該優(yōu)先使用檢查型異常。語(yǔ)言設(shè)計(jì)(編譯器強(qiáng)制您在方法簽名中列出可能被拋出的所有檢查型異常)以及早期關(guān)于樣式和用法的著作都支持該建議。最近,幾位著名的作者已經(jīng)開(kāi)始認(rèn)為非檢查型異常在優(yōu)秀的 Java 類設(shè)計(jì)中有著比以前所認(rèn)為的更為重要的地位。在本文中,Brian Goetz 考察了關(guān)于使用非檢查型異常的優(yōu)缺點(diǎn)。

  與 C++ 類似,Java 語(yǔ)言也提供異常的拋出和捕獲。但是,與 C++ 不一樣的是,Java 語(yǔ)言支持檢查型和非檢查型異常。Java 類必須在方法簽名中聲明它們所拋出的任何檢查型異常,并且對(duì)于任何方法,如果它調(diào)用的方法拋出一個(gè)類型為 E 的檢查型異常,那么它必須捕獲 E 或者也聲明為拋出 E(或者 E 的一個(gè)父類)。通過(guò)這種方式,該語(yǔ)言強(qiáng)制我們文檔化控制可能退出一個(gè)方法的所有預(yù)期方式。

  對(duì)于因?yàn)榫幊体e(cuò)誤而導(dǎo)致的異常,或者是不能期望程序捕獲的異常(解除引用一個(gè)空指針,數(shù)組越界,除零,等等),為了使開(kāi)發(fā)人員免于處理這些異常,一些異常被命名為非檢查型異常(即那些繼承自 RuntimeException 的異常)并且不需要進(jìn)行聲明。

  傳統(tǒng)的觀點(diǎn)

  在下面的來(lái)自 Sun 的“The Java Tutorial”的摘錄中,總結(jié)了關(guān)于將一個(gè)異常聲明為檢查型還是非檢查型的傳統(tǒng)觀點(diǎn)(更多的信息請(qǐng)參閱 參考資料):

  因?yàn)?Java 語(yǔ)言并不要求方法捕獲或者指定運(yùn)行時(shí)異常,因此編寫(xiě)只拋出運(yùn)行時(shí)異常的代碼或者使得他們的所有異常子類都繼承自 RuntimeException ,對(duì)于程序員來(lái)說(shuō)是有吸引力的。這些編程捷徑都允許程序員編寫(xiě) Java 代碼而不會(huì)受到來(lái)自編譯器的所有挑剔性錯(cuò)誤的干擾,并且不用去指定或者捕獲任何異常。盡管對(duì)于程序員來(lái)說(shuō)這似乎比較方便,但是它回避了 Java 的捕獲或者指定要求的意圖,并且對(duì)于那些使用您提供的類的程序員可能會(huì)導(dǎo)致問(wèn)題。

  檢查型異常代表關(guān)于一個(gè)合法指定的請(qǐng)求的操作的有用信息,調(diào)用者可能已經(jīng)對(duì)該操作沒(méi)有控制,并且調(diào)用者需要得到有關(guān)的通知 —— 例如,文件系統(tǒng)已滿,或者遠(yuǎn)端已經(jīng)關(guān)閉連接,或者訪問(wèn)權(quán)限不允許該動(dòng)作。

  如果您僅僅是因?yàn)椴幌胫付ó惓6鴴伋鲆粋(gè) RuntimeException,或者創(chuàng)建 RuntimeException 的一個(gè)子類,那么您換取到了什么呢?您只是獲得了拋出一個(gè)異常而不用您指定這樣做的能力。換句話說(shuō),這是一種用于避免文檔化方法所能拋出的異常的方式。在什么時(shí)候這是有益的?也就是說(shuō),在什么時(shí)候避免注明一個(gè)方法的行為是有益的?答案是“幾乎從不!

  換句話說(shuō),Sun 告訴我們檢查型異常應(yīng)該是準(zhǔn)則。該教程通過(guò)多種方式繼續(xù)說(shuō)明,通常應(yīng)該拋出異常,而不是 RuntimeException —— 除非您是 JVM。

  在 Effective Java: Programming Language Guide 一書(shū)中,Josh Bloch 提供了下列關(guān)于檢查型和非檢查型異常的知識(shí)點(diǎn),這些與 “The Java Tutorial” 中的建議相一致(但是并不完全嚴(yán)格一致):

  第 39 條:只為異常條件使用異常。也就是說(shuō),不要為控制流使用異常,比如,在調(diào)用 Iterator.next() 時(shí)而不是在第一次檢查 Iterator.hasNext() 時(shí)捕獲 NoSuchElementException。

  第 40 條:為可恢復(fù)的條件使用檢查型異常,為編程錯(cuò)誤使用運(yùn)行時(shí)異常。這里,Bloch 回應(yīng)傳統(tǒng)的 Sun 觀點(diǎn) —— 運(yùn)行時(shí)異常應(yīng)該只是用于指示編程錯(cuò)誤,例如違反前置條件。

  第 41 條:避免不必要的使用檢查型異常。換句話說(shuō),對(duì)于調(diào)用者不可能從其中恢復(fù)的情形,或者惟一可以預(yù)見(jiàn)的響應(yīng)將是程序退出,則不要使用檢查型異常。

  第 43 條:拋出與抽象相適應(yīng)的異常。換句話說(shuō),一個(gè)方法所拋出的異常應(yīng)該在一個(gè)抽象層次上定義,該抽象層次與該方法做什么相一致,而不一定與方法的底層實(shí)現(xiàn)細(xì)節(jié)相一致。例如,一個(gè)從文件、數(shù)據(jù)庫(kù)或者 JNDI 裝載資源的方法在不能找到資源時(shí),應(yīng)該拋出某種 ResourceNotFound 異常(通常使用異常鏈來(lái)保存隱含的原因),而不是更底層的 IOException、SQLException 或者 NamingException。

  重新考察非檢查型異常的正統(tǒng)觀點(diǎn)

  最近,幾位受尊敬的專家,包括 Bruce Eckel 和 Rod Johnson,已經(jīng)公開(kāi)聲明盡管他們最初完全同意檢查型異常的正統(tǒng)觀點(diǎn),但是他們已經(jīng)認(rèn)定排他性使用檢查型異常的想法并沒(méi)有最初看起來(lái)那樣好,并且對(duì)于許多大型項(xiàng)目,檢查型異常已經(jīng)成為一個(gè)重要的問(wèn)題來(lái)源。Eckel 提出了一個(gè)更為極端的觀點(diǎn),建議所有的異常應(yīng)該是非檢查型的;Johnson 的觀點(diǎn)要保守一些,但是仍然暗示傳統(tǒng)的優(yōu)先選擇檢查型異常是過(guò)分的。(值得一提的是,C# 的設(shè)計(jì)師在語(yǔ)言設(shè)計(jì)中選擇忽略檢查型異常,使得所有異常都是非檢查型的,因而幾乎可以肯定他們具有豐富的 Java 技術(shù)使用經(jīng)驗(yàn)。但是,后來(lái)他們的確為檢查型異常的實(shí)現(xiàn)留出了空間。)

  對(duì)于檢查型異常的一些批評(píng)

  Eckel 和 Johnson 都指出了一個(gè)關(guān)于檢查型異常的相似的問(wèn)題清單;一些是檢查型異常的內(nèi)在屬性,一些是檢查型異常在 Java 語(yǔ)言中的特定實(shí)現(xiàn)的屬性,還有一些只是簡(jiǎn)單的觀察,主要是關(guān)于檢查型異常的廣泛的錯(cuò)誤使用是如何變?yōu)橐粋(gè)嚴(yán)重的問(wèn)題,從而導(dǎo)致該機(jī)制可能需要被重新考慮。

  檢查型異常不適當(dāng)?shù)乇┞秾?shí)現(xiàn)細(xì)節(jié)

  您已經(jīng)有多少次看見(jiàn)(或者編寫(xiě))一個(gè)拋出 SQLException 或者 IOException 的方法,即使它看起來(lái)與數(shù)據(jù)庫(kù)或者文件毫無(wú)關(guān)系呢?對(duì)于開(kāi)發(fā)人員來(lái)說(shuō),在一個(gè)方法的最初實(shí)現(xiàn)中總結(jié)出可能拋出的所有異常并且將它們?cè)黾拥椒椒ǖ?throws 子句(許多 IDE 甚至幫助您執(zhí)行該任務(wù))是十分常見(jiàn)的。這種直接方法的一個(gè)問(wèn)題是它違反了 Bloch 的 第 43 條 —— 被拋出的異常所位于的抽象層次與拋出它們的方法不一致。

  一個(gè)用于裝載用戶概要的方法,在找不到用戶時(shí)應(yīng)該拋出 NoSuchUserException,而不是 SQLException —— 調(diào)用者可以很好地預(yù)料到用戶可能找不到,但是不知道如何處理 SQLException。異常鏈可以用于拋出一個(gè)更為合適的異常而不用丟棄關(guān)于底層失敗的細(xì)節(jié)(例如棧跟蹤),允許抽象層將位于它們之上的分層同位于它們之下的分層的細(xì)節(jié)隔離開(kāi)來(lái),同時(shí)保留對(duì)于調(diào)試可能有用的信息。

  據(jù)說(shuō),諸如 JDBC 包的設(shè)計(jì)采取這樣一種方式,使得它難以避免該問(wèn)題。在 JDBC 接口中的每個(gè)方法都拋出 SQLException,但是在訪問(wèn)一個(gè)數(shù)據(jù)庫(kù)的過(guò)程中可能會(huì)經(jīng)歷多種不同類型的問(wèn)題,并且不同的方法可能易受不同錯(cuò)誤模式的影響。一個(gè) SQLException 可能指示一個(gè)系統(tǒng)級(jí)問(wèn)題(不能連接到數(shù)據(jù)庫(kù))、邏輯問(wèn)題(在結(jié)果集中沒(méi)有更多的行)或者特定數(shù)據(jù)的問(wèn)題(您剛才試圖插入行的主鍵已經(jīng)存在或者違反實(shí)體完整性約束)。如果沒(méi)有犯不可原諒的嘗試分析消息正文的過(guò)失,調(diào)用者是不可能區(qū)分這些不同類型的 SQLException 的。(SQLException 的確支持用于獲取數(shù)據(jù)庫(kù)特定錯(cuò)誤代碼和 SQL 狀態(tài)變量的方法,但是在實(shí)踐中這些很少用于區(qū)分不同的數(shù)據(jù)庫(kù)錯(cuò)誤條件。)

  不穩(wěn)定的方法簽名

  不穩(wěn)定的方法簽名問(wèn)題是與前面的問(wèn)題相關(guān)的 —— 如果您只是通過(guò)一個(gè)方法傳遞異常,那么您不得不在每次改變方法的實(shí)現(xiàn)時(shí)改變它的方法簽名,以及改變調(diào)用該方法的所有代碼。一旦類已經(jīng)被部署到產(chǎn)品中,管理這些脆弱的方法簽名就變成一個(gè)昂貴的任務(wù)。然而,該問(wèn)題本質(zhì)上是沒(méi)有遵循 Bloch 提出的第 43 條的另一個(gè)癥狀。方法在遇到失敗時(shí)應(yīng)該拋出一個(gè)異常,但是該異常應(yīng)該反映該方法做什么,而不是它如何做。

  有時(shí),當(dāng)程序員對(duì)因?yàn)閷?shí)現(xiàn)的改變而導(dǎo)致從方法簽名中增加或者刪除異常感到厭煩時(shí),他們不是通過(guò)使用一個(gè)抽象來(lái)定義特定層次可能拋出的異常類型,而只是將他們的所有方法都聲明為拋出 Exception。換句話說(shuō),他們已經(jīng)認(rèn)定異常只是導(dǎo)致煩惱,并且基本上將它們關(guān)閉掉了。毋庸多言,該方法對(duì)于絕大多數(shù)可任意使用的代碼來(lái)說(shuō)通常不是一個(gè)好的錯(cuò)誤處理策略。

  難以理解的代碼

  因?yàn)樵S多方法都拋出一定數(shù)目的不同異常,錯(cuò)誤處理的代碼相對(duì)于實(shí)際的功能代碼的比率可能會(huì)偏高,使得難以找到一個(gè)方法中實(shí)際完成功能的代碼。異常是通過(guò)集中錯(cuò)誤處理來(lái)設(shè)想減小代碼的,但是一個(gè)具有三行代碼和六個(gè) catch 塊(其中每個(gè)塊只是記錄異常或者包裝并重新拋出異常)的方法看起來(lái)比較膨脹并且會(huì)使得本來(lái)簡(jiǎn)單的代碼變得模糊。