Tips
《Effective Java, Third Edition》一書英文版已經出版,這本書的第二版想必很多人都讀過,號稱Java四大名著之一,不過第二版2009年出版,到現在已經將近8年的時間,但隨着Java 6,7,8,甚至9的發布,Java語言發生了深刻的變化。
在這里第一時間翻譯成中文版。供大家學習分享之用。
書中的源代碼地址:https://github.com/jbloch/effective-java-3e-source-code
注意,書中的有些代碼里方法是基於Java 9 API中的,所以JDK 最好下載 JDK 9以上的版本。但是Java 9 只是一個過渡版本,所以建議安裝JDK 10。

49.檢查參數有效性
本章(第8章)討論了方法設計的幾個方面:如何處理參數和返回值,如何設計方法簽名以及如何記載方法文檔。 本章中的大部分內容適用於構造方法和其他普通方法。 與第4章一樣,本章重點關注可用性,健壯性和靈活性上。
大多數方法和構造方法對可以將哪些值傳遞到其對應參數中有一些限制。 例如,索引值必須是非負數,對象引用必須為非null。 你應該清楚地在文檔中記載所有這些限制,並在方法主體的開頭用檢查來強制執行。 應該嘗試在錯誤發生后盡快檢測到錯誤,這是一般原則的特殊情況。 如果不這樣做,則不太可能檢測到錯誤,並且一旦檢測到錯誤就更難確定錯誤的來源。
如果將無效參數值傳遞給方法,並且該方法在執行之前檢查其參數,則它拋出適當的異常然后快速且清楚地以失敗結束。 如果該方法無法檢查其參數,可能會發生一些事情。 在處理過程中,該方法可能會出現令人困惑的異常。 更糟糕的是,該方法可以正常返回,但默默地計算錯誤的結果。 最糟糕的是,該方法可以正常返回但是將某個對象置於受損狀態,在將來某個未確定的時間在代碼中的某些不相關點處導致錯誤。 換句話說,驗證參數失敗可能導致違反故障原子性(failure atomicity )(條目 76)。
對於公共方法和受保護方法,請使用Java文檔@throws注解來記在在違反參數值限制時將引發的異常(條目 74)。 通常,生成的異常是IllegalArgumentException,IndexOutOfBoundsException或NullPointerException(條目 72)。 一旦記錄了對方法參數的限制,並且記錄了違反這些限制時將引發的異常,那么強制執行這些限制就很簡單了。 這是一個典型的例子:
/**
* Returns a BigInteger whose value is (this mod m). This method
* differs from the remainder method in that it always returns a
* non-negative BigInteger.
*
* @param m the modulus, which must be positive
* @return this mod m
* @throws ArithmeticException if m is less than or equal to 0
*/
public BigInteger mod(BigInteger m) {
if (m.signum() <= 0)
throw new ArithmeticException("Modulus <= 0: " + m);
... // Do the computation
}
請注意,文檔注釋沒有說“如果m為null,mod拋出NullPointerException”,盡管該方法正是這樣做的,這是調用m.sgn()的副產品。這個異常記載在類級別文檔注釋中,用於包含的BigInteger類。類級別的注釋應用於類的所有公共方法中的所有參數。這是避免在每個方法上分別記錄每個NullPointerException的好方法。它可以與@Nullable或類似的注釋結合使用,以表明某個特定參數可能為空,但這種做法不是標准的,為此使用了多個注解。
在Java 7中添加的Objects.requireNonNull方法靈活方便,因此沒有理由再手動執行空值檢查。 如果願意,可以指定自定義異常詳細消息。 該方法返回其輸入的值,因此可以在使用值的同時執行空檢查:
// Inline use of Java's null-checking facility
this.strategy = Objects.requireNonNull(strategy, "strategy");
你也可以忽略返回值,並使用Objects.requireNonNull作為滿足需求的獨立空值檢查。
在Java 9中,java.util.Objects類中添加了范圍檢查工具。 此工具包含三個方法:checkFromIndexSize,checkFromToIndex和checkIndex。 此工具不如空檢查方法靈活。 它不允許指定自己的異常詳細消息,它僅用於列表和數組索引。 它不處理閉合范圍(包含兩個端點)。 但如果它能滿足你的需要,那就很方便了。
對於未導出的方法,作為包的作者,控制調用方法的環境,這樣就可以並且應該確保只傳入有效的參數值。因此,非公共方法可以使用斷言檢查其參數,如下所示:
// Private helper function for a recursive sort
private static void sort(long a[], int offset, int length) {
assert a != null;
assert offset >= 0 && offset <= a.length;
assert length >= 0 && length <= a.length - offset;
... // Do the computation
}
本質上,這些斷言聲稱斷言條件將成立,無論其客戶端如何使用封閉包。與普通的有效性檢查不同,斷言如果失敗會拋出AssertionError。與普通的有效性檢查不同的是,除非使用-ea(或者-enableassertions)標記傳遞給java命令來啟用它們,否則它們不會產生任何效果,本質上也不會產生任何成本。有關斷言的更多信息,請參閱教程assert。
檢查方法中未使用但存儲以供以后使用的參數的有效性尤為重要。例如,考慮第101頁上的靜態工廠方法,它接受一個int數組並返回數組的List視圖。如果客戶端傳入null,該方法將拋出NullPointerException,因為該方法具有顯式檢查(調用Objects.requireNonNull方法)。如果省略了該檢查,則該方法將返回對新創建的List實例的引用,該實例將在客戶端嘗試使用它時立即拋出NullPointerException。 到那時,List實例的來源可能很難確定,這可能會使調試任務大大復雜化。
構造方法是這個原則的一個特例,你應該檢查要存儲起來供以后使用的參數的有效性。檢查構造方法參數的有效性對於防止構造對象違反類不變性(class invariants)非常重要。
你應該在執行計算之前顯式檢查方法的參數,但這一規則也有例外。 一個重要的例外是有效性檢查昂貴或不切實際的情況,並且在進行計算的過程中隱式執行檢查。 例如,考慮一種對對象列表進行排序的方法,例如Collections.sort(List)。 列表中的所有對象必須是可相互比較的。 在對列表進行排序的過程中,列表中的每個對象都將與其他對象進行比較。 如果對象不可相互比較,則某些比較操作拋出ClassCastException異常,這正是sort方法應該執行的操作。 因此,提前檢查列表中的元素是否具有可比性是沒有意義的。 但請注意,不加選擇地依賴隱式有效性檢查會導致失敗原子性( failure atomicity)的丟失(條目 76)。
有時,計算會隱式執行必需的有效性檢查,但如果檢查失敗則會拋出錯誤的異常。 換句話說,計算由於無效參數值而自然拋出的異常與文檔記錄方法拋出的異常不匹配。 在這些情況下,你應該使用條目 73中描述的異常翻譯( exception translation)習慣用法將自然異常轉換為正確的異常。
不要從本條目中推斷出對參數的任意限制都是一件好事。 相反,你應該設計一些方法,使其盡可能通用。 假設方法可以對它接受的所有參數值做一些合理的操作,那么對參數的限制越少越好。 但是,通常情況下,某些限制是正在實現的抽象所固有的。
總而言之,每次編寫方法或構造方法時,都應該考慮對其參數存在哪些限制。 應該記在這些限制,並在方法體的開頭使用顯式檢查來強制執行這些限制。 養成這樣做的習慣很重要。 在第一次有效性檢查失敗時,它所需要的少量工作將會得到對應的回報。
