天天看點

Effective Java 第三版——69. 僅在發生異常的條件下使用異常

Tips

書中的源代碼位址:https://github.com/jbloch/effective-java-3e-source-code

注意,書中的有些代碼裡方法是基于Java 9 API中的,是以JDK 最好下載下傳 JDK 9以上的版本。

異常

當充分發揮異常的優勢時,它可以提高程式的可讀性、可靠性和可維護性。如果使用不當,則會産生相反的效果。本章提供了有效使用異常的指南。

69. 僅在發生異常的條件下使用異常

有一天,如果你運氣不好,你可能會偶然發現這樣一段代碼:

// Horrible abuse of exceptions. Don't ever do this!
try {
    int i = 0;
    while(true)
        range[i++].climb();
} catch (ArrayIndexOutOfBoundsException e) {
}
           

這段代碼是做什麼的?檢查結果看來一點也不明顯,這就是不使用它的充分理由(條目 67)。事實證明,這是一種用于循環周遊數組元素的非常錯誤的習慣用法。當試圖通路數組邊界之外的第一個數組元素時,無限循環通過抛出、捕獲和忽略

ArrayIndexOutOfBoundsException

異常來終止。它應該等同于循環數組的标準習慣用法,任何Java程式員都可以一眼就能識别出來:

for (Mountain m : range)
    m.climb();
           

那麼為什麼有人會使用基于異常的循環而不是嘗試和正确的用法? 根據錯誤推理提高性能是一種錯誤的嘗試,因為虛拟機檢查所有數組通路的邊界,由編譯器隐藏但仍然存在于for-each循環中的正常循環終止測試是多餘的,應該避免。 這個推理有三個問題:

  • 因為異常是為特殊情況設計的,是以JVM實作者幾乎沒有試圖讓它們像顯式測試一樣快。
  • 将代碼放在try-catch塊中會抑制虛拟機實作可能執行的某些優化。
  • 周遊數組的标準習慣用法不一定會導緻備援檢查。許多虛拟機實作對它們進行了優化。

事實上,基于異常的習慣用法比标準用法慢得多。在我的機器上,100個元素的數組,基于異常的習慣用法的速度大約是标準習慣用法的兩倍。

基于異常的循環不僅混淆了代碼的目的,降低了代碼的性能,而且不能保證它能正常工作。如果循環中存在bug,使用異常進行流控制可以掩蓋該bug,進而大大增加調試過程的複雜性。假設循環體中的計算調用一個方法,該方法對一些不相關的數組執行越界通路。如果使用合理的循環習慣用法,該bug将生成一個未捕獲的異常,導緻線程立即終止,并帶有完整的堆棧跟蹤。如果使用錯誤的基于異常的循環,則會捕獲與bug相關的異常,并将其誤解為正常的循環終止。

這個示例說明的道理很簡單:顧名思義,異常僅用于特殊情況; 它們永遠不應該用于正常的控制流程。 通常來說,使用标準的、易于識别的習慣用法,而不是聲稱可以提供更好性能的過度聰明的技術。即使性能優勢是真實存在的,但在穩步改進平台實作的情況下,這種優勢也可能不複存在。然而,來自過度聰明的技術的細微缺陷和維護難題肯定會繼續存在。

這個原則對API設計也有影響。一個設計良好的API不能強迫它的用戶端為正常的控制流使用異常。隻有在某些不可預知的條件下才能調用具有“狀态依賴(state-dependent)”方法的類,通常應該有一個單獨的“狀态測試(state-testing)”方法,訓示是否适合調用狀态依賴方法。例如,Iterator接口具有依賴于狀态的next方法和對應的狀态測試方法hasNext。這支援使用傳統for循環(以及for-each循環,其中内部使用了hasNext方法)在集合上疊代的标準習慣用法:

for (Iterator<Foo> i = collection.iterator(); i.hasNext(); ) {
    Foo foo = i.next();
    ...
}
           

如果Iterator缺少hasNext方法,則用戶端将被迫執行此操作:

// Do not use this hideous code for iteration over a collection!
try {
    Iterator<Foo> i = collection.iterator();
    while(true) {
        Foo foo = i.next();
        ...
    }
} catch (NoSuchElementException e) {
}
           

這數組疊代的例子非常類似于本條目一開始的那個例子。除了冗長和誤導之外,基于異常的循環很可能執行得很差,并且可以掩蓋系統中不相關部分中的bug。

提供單獨的狀态測試方法的另一種方式是,讓依賴于狀态的方法傳回一個空的Optional值(條目 55),或者在它不能執行所需的計算時傳回一個區分值,比如null。

下面是一些指導原則,幫助你在狀态測試方法,Optional的或區分的傳回值之間進行選擇。如果要在沒有外部同步的情況下并發地通路對象,或者受制于外部引發的狀态轉換,則必須使用Optional的或可區分的傳回值,因為在調用狀态測試方法與其依賴于狀态的方法之間的間隔内,對象的狀态可能會發生變化。如果一個單獨的狀态測試方法将重複依賴于狀态的方法的工作,那麼性能問題可能要求使用一個Optional的或可區分的傳回值。在所有其他條件相同的情況下,狀态測試方法略優于區分的傳回值。它提供了更好的可讀性,而且不正确的使用可能更容易檢測:如果忘記調用狀态測試方法,依賴于狀态的方法将抛出異常,使錯誤變得明顯;如果忘記檢查一個可區分的傳回值,那麼這個bug可能很微妙。這不是Optional傳回值的問題。

總之,異常是針對特殊情況而設計的。不要将它們用于正常的控制流程,也不要編寫強制其他人這樣做的API。