Categories
程式開發

Java最大的錯誤:檢查異常


本文最初發佈於literatejava.com網站,經原作者授權由InfoQ中文站翻譯並分享。

檢查異常(checked exception)一直是Java語言中一個有爭議的特性。

擁護者聲稱,這一特性可以確保故障檢查並從故障中恢復。而批評者則說,“catch”塊幾乎永遠無法從異常中恢復,並且是錯誤的常見來源。

今天,Java 8和lambda已經出現了。在Java世界中,檢查異常是否已經過時了呢?

檢查異常的意圖

在90年代中期,Sun公司的James Gosling提出了一種新的語言。

當時,C++編程要求每個返回的函數都要檢查是否有錯誤。他認為必須找出更好的方法,並因此將“異常”的概念構建到Java語言中。

檢查異常的目的是在本地標誌,並強制開發人員處理可能的異常。已檢查的異常必須在方法簽名上聲明或處理。

這是為了增強軟件的可靠性和彈性。人們希望從意外狀況中“恢復”——提供可預測的失敗結果,例如嘗試付款時發生的InsufficientFundsException。關於實際上需要怎樣的“恢復”工作,當時的人們並不清楚。

運行時異常(runtime exception)也包含進了Java中。由於空指針、數據錯誤和非法狀態/訪問都可能在代碼中的任何地方發生,因此將它們作為運行時異常的子類型。

運行時異常可以在任何地方拋出,而無需聲明,並且更加方便。但是改用它們是否是正確的選擇呢?

缺點

這裡的關鍵在於運行時和檢查異常在功能上是等效的。沒有什麼處理或恢復是檢查異常能做,而運行時異常做不了的。

反對“檢查的”異常的最大論點是,大多數異常無法修復。一個簡單的事實是,我們無法控制損壞的代碼/子系統。我們看不到實現,對此不承擔任何責任,也無法修復它。

尤其成問題的是JDBC(SQLException)和EJB的RMI(RemoteException)。這些帶來了普遍存在的、系統性的、實際上無法修復的可靠性問題,並不會像原始的“受檢查的異常”理念確定可修復的突發事件。

對於任何方法,它調用的所有子方法都會有失敗的可能性。潛在的故障會累積在調用樹中。在方法簽名上聲明它們已經無法為開發人員提供需要關注的重點位置了——因為聲明的異常已經遍布整個調用樹。

大多數EJB開發人員都經歷過這種情況——通過層或整個代碼庫的方法都需要聲明的異常。調用一個具有不同異常的方法需要調整成打方法。

許多開發人員被告知要捕獲低級別的異常,然後將其重新拋出為更高級別(應用程序級別)的檢查異常。這需要大量的(每個項目可多達2000個)非功能性的“catch-throw”代碼塊。

吞下異常、隱藏原因、重複記錄以及返回“空”/未初始化的數據,這些都是很常見的情況。大多數項目可以統計出600多個錯誤的編碼或完全錯誤。

最終,開發人員開始抵觸大量的“catch”代碼塊,不想再看到它們成為錯誤的來源。

檢查異常——與函數式編程不兼容

然後,我們來看看Java 8,它具有新的函數式編程特性——例如lambda、Stream和函數組合。

這些特性建立在泛型的基礎上——參數和返回類型已泛化,因此無論項目類型如何,我們都可以編寫執行通用操作的迭代和流操作(forEach、map、flatMap)。

但與數據類型不同,聲明的異常無法泛化。

在Java中,這樣的流操作是不可行的(例如Stream.map):它需要一個lambda來聲明某些已檢查的異常,並透明地將這個檢查異常傳遞給周圍的代碼。

這一直是反對檢查異常的主要論據——拋出和接收“catch”塊之間介入的所有代碼都必須意識到異常。

解決方法是將其“包裝”在一個運行時異常中,從而隱藏異常的原始類型——這讓原始概念中設想的特定於異常的“catch”塊失去了用武之地。

最後,我們注意到Java 8中沒有任何新的“函數式接口”聲明檢查異常,因此可以說Java的新理念已經和以前不同了。

結論

與之前的語言相比,Java異常在可靠性和錯誤處理方面提供了重要優勢。 Java提供了可靠的服務器和商業軟件,使用的是C/C++永遠無法做到的方式。

檢查異常從其出發點來看,是處理“突發事件”而不是“失敗”的嘗試。其明確目標是高亮顯示特定的可預測點(無法連接、找不到文件等)並確保開發人員能夠處理這些點。

最初的概念從未包含在內的是,要強制聲明發生了大量系統性和不可恢復的故障。這些失敗永遠都不是正確的,不能被聲明為檢查異常。

通常,代碼中可能會發生故障,而EJB、Web和Swing/AWT容器已經通過提供最外部的“失敗請求”異常處理程序來解決這個問題。最基本的正確策略是回滾事務並返回錯誤。

運行時異常允許對檢查異常進行任何異常處理,但避免了限制性的編碼約束。這簡化了代碼,並使其更容易遵循儘早拋出、較晚捕獲,在最外層/最高級別處理異常的最佳實踐。

主流Java框架和影響者現在已經明確地擺脫了檢查異常。 Spring、Hibernate和現代Java框架/供應商僅使用運行時異常,而這種便利性是它們流行的主要因素。

像是Josh Bloch(Java Collections框架)、Rod Johnson、Anders Hejlsberg(C#之父)、Gavin King和Stephen Colebourn(JodaTime)等大牛都反對檢查異常。

現在,在Java 8中,lambda是向前邁出的關鍵一步。這些語言特性從內部的函數式操作中抽像出“控制流”。如我們所見,這使得檢查異常和“立即聲明或處理”的要求過時了。

對於開發人員而言,關注可靠性並診斷可能的故障點(突發事件)(例如文件打開、數據庫連接等)一直都是很重要的事情。如果我們在此時提供良好的錯誤消息,我們就能創建自診斷軟件——這會是工程成就的巔峰之作。

但我們應該使用未檢查的異常來做到這一點,並且如果必須重新拋出,則應始終使用運行時異常或特定於應用程序的子類。

正如Stephen Colebourn所說,如果你的項目仍在使用或提倡檢查異常,則你的技能已過時5-10年了。 Java已經前進了很長的路程。

原文鏈接:

http://literatejava.com/exceptions/checked-exceptions-javas-biggest-mistake