Java 理論與實戰(zhàn): 對于異常的爭論
發(fā)表時間:2024-01-17 來源:明輝站整理相關(guān)軟件相關(guān)文章人氣:
[摘要]關(guān)于在 Java 語言中使用異常的大多數(shù)建議都認為,在確信異?梢员徊东@的任何情況下,應(yīng)該優(yōu)先使用檢查型異常。語言設(shè)計(編譯器強制您在方法簽名中列出可能被拋出的所有檢查型異常)以及早期關(guān)于樣式和用法的著作都支持該建議。最近,幾位著名的作者已經(jīng)開始認為非檢查型異常在優(yōu)秀的 Java 類設(shè)計中有著比以...
關(guān)于在 Java 語言中使用異常的大多數(shù)建議都認為,在確信異?梢员徊东@的任何情況下,應(yīng)該優(yōu)先使用檢查型異常。語言設(shè)計(編譯器強制您在方法簽名中列出可能被拋出的所有檢查型異常)以及早期關(guān)于樣式和用法的著作都支持該建議。最近,幾位著名的作者已經(jīng)開始認為非檢查型異常在優(yōu)秀的 Java 類設(shè)計中有著比以前所認為的更為重要的地位。在本文中,Brian Goetz 考察了關(guān)于使用非檢查型異常的優(yōu)缺點。
與 C++ 類似,Java 語言也提供異常的拋出和捕獲。但是,與 C++ 不一樣的是,Java 語言支持檢查型和非檢查型異常。Java 類必須在方法簽名中聲明它們所拋出的任何檢查型異常,并且對于任何方法,如果它調(diào)用的方法拋出一個類型為 E 的檢查型異常,那么它必須捕獲 E 或者也聲明為拋出 E(或者 E 的一個父類)。通過這種方式,該語言強制我們文檔化控制可能退出一個方法的所有預期方式。
對于因為編程錯誤而導致的異常,或者是不能期望程序捕獲的異常(解除引用一個空指針,數(shù)組越界,除零,等等),為了使開發(fā)人員免于處理這些異常,一些異常被命名為非檢查型異常(即那些繼承自 RuntimeException 的異常)并且不需要進行聲明。
傳統(tǒng)的觀點 在下面的來自 Sun 的“The Java Tutorial”的摘錄中,總結(jié)了關(guān)于將一個異常聲明為檢查型還是非檢查型的傳統(tǒng)觀點(更多的信息請參閱 參考資料):
因為 Java 語言并不要求方法捕獲或者指定運行時異常,因此編寫只拋出運行時異常的代碼或者使得他們的所有異常子類都繼承自 RuntimeException ,對于程序員來說是有吸引力的。這些編程捷徑都允許程序員編寫 Java 代碼而不會受到來自編譯器的所有挑剔性錯誤的干擾,并且不用去指定或者捕獲任何異常。盡管對于程序員來說這似乎比較方便,但是它回避了 Java 的捕獲或者指定要求的意圖,并且對于那些使用您提供的類的程序員可能會導致問題。
檢查型異常代表關(guān)于一個合法指定的請求的操作的有用信息,調(diào)用者可能已經(jīng)對該操作沒有控制,并且調(diào)用者需要得到有關(guān)的通知 —— 例如,文件系統(tǒng)已滿,或者遠端已經(jīng)關(guān)閉連接,或者訪問權(quán)限不允許該動作。
如果您僅僅是因為不想指定異常而拋出一個 RuntimeException,或者創(chuàng)建 RuntimeException 的一個子類,那么您換取到了什么呢?您只是獲得了拋出一個異常而不用您指定這樣做的能力。換句話說,這是一種用于避免文檔化方法所能拋出的異常的方式。在什么時候這是有益的?也就是說,在什么時候避免注明一個方法的行為是有益的?答案是“幾乎從不!
換句話說,Sun 告訴我們檢查型異常應(yīng)該是準則。該教程通過多種方式繼續(xù)說明,通常應(yīng)該拋出異常,而不是 RuntimeException —— 除非您是 JVM。
在 Effective Java: Programming Language Guide 一書中,Josh Bloch 提供了下列關(guān)于檢查型和非檢查型異常的知識點,這些與 “The Java Tutorial” 中的建議相一致(但是并不完全嚴格一致):
第 39 條:只為異常條件使用異常。也就是說,不要為控制流使用異常,比如,在調(diào)用 Iterator.next() 時而不是在第一次檢查 Iterator.hasNext() 時捕獲 NoSuchElementException。
第 40 條:為可恢復的條件使用檢查型異常,為編程錯誤使用運行時異常。這里,Bloch 回應(yīng)傳統(tǒng)的 Sun 觀點 —— 運行時異常應(yīng)該只是用于指示編程錯誤,例如違反前置條件。
第 41 條:避免不必要的使用檢查型異常。換句話說,對于調(diào)用者不可能從其中恢復的情形,或者惟一可以預見的響應(yīng)將是程序退出,則不要使用檢查型異常。
第 43 條:拋出與抽象相適應(yīng)的異常。換句話說,一個方法所拋出的異常應(yīng)該在一個抽象層次上定義,該抽象層次與該方法做什么相一致,而不一定與方法的底層實現(xiàn)細節(jié)相一致。例如,一個從文件、數(shù)據(jù)庫或者 JNDI 裝載資源的方法在不能找到資源時,應(yīng)該拋出某種 ResourceNotFound 異常(通常使用異常鏈來保存隱含的原因),而不是更底層的 IOException、SQLException 或者 NamingException。
重新考察非檢查型異常的正統(tǒng)觀點 最近,幾位受尊敬的專家,包括 Bruce Eckel 和 Rod Johnson,已經(jīng)公開聲明盡管他們最初完全同意檢查型異常的正統(tǒng)觀點,但是他們已經(jīng)認定排他性使用檢查型異常的想法并沒有最初看起來那樣好,并且對于許多大型項目,檢查型異常已經(jīng)成為一個重要的問題來源。Eckel 提出了一個更為極端的觀點,建議所有的異常應(yīng)該是非檢查型的;Johnson 的觀點要保守一些,但是仍然暗示傳統(tǒng)的優(yōu)先選擇檢查型異常是過分的。(值得一提的是,C# 的設(shè)計師在語言設(shè)計中選擇忽略檢查型異常,使得所有異常都是非檢查型的,因而幾乎可以肯定他們具有豐富的 Java 技術(shù)使用經(jīng)驗。但是,后來他們的確為檢查型異常的實現(xiàn)留出了空間。)
對于檢查型異常的一些批評
Eckel 和 Johnson 都指出了一個關(guān)于檢查型異常的相似的問題清單;一些是檢查型異常的內(nèi)在屬性,一些是檢查型異常在 Java 語言中的特定實現(xiàn)的屬性,還有一些只是簡單的觀察,主要是關(guān)于檢查型異常的廣泛的錯誤使用是如何變?yōu)橐粋嚴重的問題,從而導致該機制可能需要被重新考慮。
檢查型異常不適當?shù)乇┞秾崿F(xiàn)細節(jié) 您已經(jīng)有多少次看見(或者編寫)一個拋出 SQLException 或者 IOException 的方法,即使它看起來與數(shù)據(jù)庫或者文件毫無關(guān)系呢?對于開發(fā)人員來說,在一個方法的最初實現(xiàn)中總結(jié)出可能拋出的所有異常并且將它們增加到方法的 throws 子句(許多 IDE 甚至幫助您執(zhí)行該任務(wù))是十分常見的。這種直接方法的一個問題是它違反了 Bloch 的 第 43 條 —— 被拋出的異常所位于的抽象層次與拋出它們的方法不一致。
一個用于裝載用戶概要的方法,在找不到用戶時應(yīng)該拋出 NoSuchUserException,而不是 SQLException —— 調(diào)用者可以很好地預料到用戶可能找不到,但是不知道如何處理 SQLException。異常鏈可以用于拋出一個更為合適的異常而不用丟棄關(guān)于底層失敗的細節(jié)(例如棧跟蹤),允許抽象層將位于它們之上的分層同位于它們之下的分層的細節(jié)隔離開來,同時保留對于調(diào)試可能有用的信息。
據(jù)說,諸如 JDBC 包的設(shè)計采取這樣一種方式,使得它難以避免該問題。在 JDBC 接口中的每個方法都拋出 SQLException,但是在訪問一個數(shù)據(jù)庫的過程中可能會經(jīng)歷多種不同類型的問題,并且不同的方法可能易受不同錯誤模式的影響。一個 SQLException 可能指示一個系統(tǒng)級問題(不能連接到數(shù)據(jù)庫)、邏輯問題(在結(jié)果集中沒有更多的行)或者特定數(shù)據(jù)的問題(您剛才試圖插入行的主鍵已經(jīng)存在或者違反實體完整性約束)。如果沒有犯不可原諒的嘗試分析消息正文的過失,調(diào)用者是不可能區(qū)分這些不同類型的 SQLException 的。(SQLException 的確支持用于獲取數(shù)據(jù)庫特定錯誤代碼和 SQL 狀態(tài)變量的方法,但是在實踐中這些很少用于區(qū)分不同的數(shù)據(jù)庫錯誤條件。)
不穩(wěn)定的方法簽名 不穩(wěn)定的方法簽名問題是與前面的問題相關(guān)的 —— 如果您只是通過一個方法傳遞異常,那么您不得不在每次改變方法的實現(xiàn)時改變它的方法簽名,以及改變調(diào)用該方法的所有代碼。一旦類已經(jīng)被部署到產(chǎn)品中,管理這些脆弱的方法簽名就變成一個昂貴的任務(wù)。然而,該問題本質(zhì)上是沒有遵循 Bloch 提出的第 43 條的另一個癥狀。方法在遇到失敗時應(yīng)該拋出一個異常,但是該異常應(yīng)該反映該方法做什么,而不是它如何做。
有時,當程序員對因為實現(xiàn)的改變而導致從方法簽名中增加或者刪除異常感到厭煩時,他們不是通過使用一個抽象來定義特定層次可能拋出的異常類型,而只是將他們的所有方法都聲明為拋出 Exception。換句話說,他們已經(jīng)認定異常只是導致煩惱,并且基本上將它們關(guān)閉掉了。毋庸多言,該方法對于絕大多數(shù)可任意使用的代碼來說通常不是一個好的錯誤處理策略。
難以理解的代碼 因為許多方法都拋出一定數(shù)目的不同異常,錯誤處理的代碼相對于實際的功能代碼的比率可能會偏高,使得難以找到一個方法中實際完成功能的代碼。異常是通過集中錯誤處理來設(shè)想減小代碼的,但是一個具有三行代碼和六個 catch 塊(其中每個塊只是記錄異常或者包裝并重新拋出異常)的方法看起來比較膨脹并且會使得本來簡單的代碼變得模糊。