Java 理論與實踐: 關於異常的爭論

azz發表於2007-08-24
Java 理論與實踐: 關於異常的爭論[@more@]  關於在 Java 語言中使用異常的大多數建議都認為,在確信異常可以被捕獲的任何情況下,應該優先使用檢查型異常。語言設計(編譯器強制您在方法簽名中列出可能被丟擲的所有檢查型異常)以及早期關於樣式和用法的著作都支援該建議。最近,幾位著名的作者已經開始認為非檢查型異常在優秀的 Java 類設計中有著比以前所認為的更為重要的地位。在本文中,Brian Goetz 考察了關於使用非檢查型異常的優缺點。

  與 C++ 類似,Java 語言也提供異常的丟擲和捕獲。但是,與 C++ 不一樣的是,Java 語言支援檢查型和非檢查型異常。Java 類必須在方法簽名中宣告它們所丟擲的任何檢查型異常,並且對於任何方法,如果它呼叫的方法丟擲一個型別為 E 的檢查型異常,那麼它必須捕獲 E 或者也宣告為丟擲 E(或者 E 的一個父類)。透過這種方式,該語言強制我們文件化控制可能退出一個方法的所有預期方式。

  對於因為程式設計錯誤而導致的異常,或者是不能期望程式捕獲的異常(解除引用一個空指標,陣列越界,除零,等等),為了使開發人員免於處理這些異常,一些異常被命名為非檢查型異常(即那些繼承自 RuntimeException 的異常)並且不需要進行宣告。

  傳統的觀點

  在下面的來自 Sun 的“The Java Tutorial”的摘錄中,總結了關於將一個異常宣告為檢查型還是非檢查型的傳統觀點(更多的資訊請參閱 參考資料):

  因為 Java 語言並不要求方法捕獲或者指定執行時異常,因此編寫只丟擲執行時異常的程式碼或者使得他們的所有異常子類都繼承自 RuntimeException ,對於程式設計師來說是有吸引力的。這些程式設計捷徑都允許程式設計師編寫 Java 程式碼而不會受到來自編譯器的所有挑剔性錯誤的干擾,並且不用去指定或者捕獲任何異常。儘管對於程式設計師來說這似乎比較方便,但是它迴避了 Java 的捕獲或者指定要求的意圖,並且對於那些使用您提供的類的程式設計師可能會導致問題。

  檢查型異常代表關於一個合法指定的請求的操作的有用資訊,呼叫者可能已經對該操作沒有控制,並且呼叫者需要得到有關的通知 —— 例如,檔案系統已滿,或者遠端已經關閉連線,或者訪問許可權不允許該動作。

  如果您僅僅是因為不想指定異常而丟擲一個 RuntimeException,或者建立 RuntimeException 的一個子類,那麼您換取到了什麼呢?您只是獲得了丟擲一個異常而不用您指定這樣做的能力。換句話說,這是一種用於避免文件化方法所能丟擲的異常的方式。在什麼時候這是有益的?也就是說,在什麼時候避免註明一個方法的行為是有益的?答案是“幾乎從不。”

  換句話說,Sun 告訴我們檢查型異常應該是準則。該教程透過多種方式繼續說明,通常應該丟擲異常,而不是 RuntimeException —— 除非您是 JVM。

  在 Effective Java: Programming Language Guide 一書中,Josh Bloch 提供了下列關於檢查型和非檢查型異常的知識點,這些與 “The Java Tutorial” 中的建議相一致(但是並不完全嚴格一致):

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

  第 40 條:為可恢復的條件使用檢查型異常,為程式設計錯誤使用執行時異常。這裡,Bloch 回應傳統的 Sun 觀點 —— 執行時異常應該只是用於指示程式設計錯誤,例如違反前置條件。

  第 41 條:避免不必要的使用檢查型異常。換句話說,對於呼叫者不可能從其中恢復的情形,或者惟一可以預見的響應將是程式退出,則不要使用檢查型異常。

  第 43 條:丟擲與抽象相適應的異常。換句話說,一個方法所丟擲的異常應該在一個抽象層次上定義,該抽象層次與該方法做什麼相一致,而不一定與方法的底層實現細節相一致。例如,一個從檔案、資料庫或者 JNDI 裝載資源的方法在不能找到資源時,應該丟擲某種 ResourceNotFound 異常(通常使用異常鏈來儲存隱含的原因),而不是更底層的 IOException、SQLException 或者 NamingException。

  重新考察非檢查型異常的正統觀點

  最近,幾位受尊敬的專家,包括 Bruce Eckel 和 Rod Johnson,已經公開宣告儘管他們最初完全同意檢查型異常的正統觀點,但是他們已經認定排他性使用檢查型異常的想法並沒有最初看起來那樣好,並且對於許多大型專案,檢查型異常已經成為一個重要的問題來源。Eckel 提出了一個更為極端的觀點,建議所有的異常應該是非檢查型的;Johnson 的觀點要保守一些,但是仍然暗示傳統的優先選擇檢查型異常是過分的。(值得一提的是,C# 的設計師在語言設計中選擇忽略檢查型異常,使得所有異常都是非檢查型的,因而幾乎可以肯定他們具有豐富的 Java 技術使用經驗。但是,後來他們的確為檢查型異常的實現留出了空間。)

  對於檢查型異常的一些批評

  Eckel 和 Johnson 都指出了一個關於檢查型異常的相似的問題清單;一些是檢查型異常的內在屬性,一些是檢查型異常在 Java 語言中的特定實現的屬性,還有一些只是簡單的觀察,主要是關於檢查型異常的廣泛的錯誤使用是如何變為一個嚴重的問題,從而導致該機制可能需要被重新考慮。

  檢查型異常不適當地暴露實現細節

  您已經有多少次看見(或者編寫)一個丟擲 SQLException 或者 IOException 的方法,即使它看起來與資料庫或者檔案毫無關係呢?對於開發人員來說,在一個方法的最初實現中總結出可能丟擲的所有異常並且將它們增加到方法的 throws 子句(許多 IDE 甚至幫助您執行該任務)是十分常見的。這種直接方法的一個問題是它違反了 Bloch 的 第 43 條 —— 被丟擲的異常所位於的抽象層次與丟擲它們的方法不一致。

  一個用於裝載使用者概要的方法,在找不到使用者時應該丟擲 NoSuchUserException,而不是 SQLException —— 呼叫者可以很好地預料到使用者可能找不到,但是不知道如何處理 SQLException。異常鏈可以用於丟擲一個更為合適的異常而不用丟棄關於底層失敗的細節(例如棧跟蹤),允許抽象層將位於它們之上的分層同位於它們之下的分層的細節隔離開來,同時保留對於除錯可能有用的資訊。

  據說,諸如 JDBC 包的設計採取這樣一種方式,使得它難以避免該問題。在 JDBC 介面中的每個方法都丟擲 SQLException,但是在訪問一個資料庫的過程中可能會經歷多種不同型別的問題,並且不同的方法可能易受不同錯誤模式的影響。一個 SQLException 可能指示一個系統級問題(不能連線到資料庫)、邏輯問題(在結果集中沒有更多的行)或者特定資料的問題(您剛才試圖插入行的主鍵已經存在或者違反實體完整性約束)。如果沒有犯不可原諒的嘗試分析訊息正文的過失,呼叫者是不可能區分這些不同型別的 SQLException 的。(SQLException 的確支援用於獲取資料庫特定錯誤程式碼和 SQL 狀態變數的方法,但是在實踐中這些很少用於區分不同的資料庫錯誤條件。)

  不穩定的方法簽名

  不穩定的方法簽名問題是與前面的問題相關的 —— 如果您只是透過一個方法傳遞異常,那麼您不得不在每次改變方法的實現時改變它的方法簽名,以及改變呼叫該方法的所有程式碼。一旦類已經被部署到產品中,管理這些脆弱的方法簽名就變成一個昂貴的任務。然而,該問題本質上是沒有遵循 Bloch 提出的第 43 條的另一個症狀。方法在遇到失敗時應該丟擲一個異常,但是該異常應該反映該方法做什麼,而不是它如何做。

  有時,當程式設計師對因為實現的改變而導致從方法簽名中增加或者刪除異常感到厭煩時,他們不是透過使用一個抽象來定義特定層次可能丟擲的異常型別,而只是將他們的所有方法都宣告為丟擲 Exception。換句話說,他們已經認定異常只是導致煩惱,並且基本上將它們關閉掉了。毋庸多言,該方法對於絕大多數可任意使用的程式碼來說通常不是一個好的錯誤處理策略。

  難以理解的程式碼

  因為許多方法都丟擲一定數目的不同異常,錯誤處理的程式碼相對於實際的功能程式碼的比率可能會偏高,使得難以找到一個方法中實際完成功能的程式碼。異常是透過集中錯誤處理來設想減小程式碼的,但是一個具有三行程式碼和六個 catch 塊(其中每個塊只是記錄異常或者包裝並重新丟擲異常)的方法看起來比較膨脹並且會使得本來簡單的程式碼變得模糊。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10901326/viewspace-965620/,如需轉載,請註明出處,否則將追究法律責任。

相關文章