Effective Java 第三版——69. 遵守普遍接受的命名約定
Tips
書中的原始碼地址: https://github.com/jbloch/effective-java-3e-source-code
注意,書中的有些程式碼裡方法是基於Java 9 API中的,所以JDK 最好下載 JDK 9以上的版本。

Effective Java, Third Edition
異常
當充分發揮異常的優勢時,它可以提高程式的可讀性、可靠性和可維護性。如果使用不當,則會產生相反的效果。本章提供了有效使用異常的指南。
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。