1. 程式人生 > >Effective Java 第三版——49. 檢查引數有效性

Effective Java 第三版——49. 檢查引數有效性

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。

Effective Java, Third Edition

49.檢查引數有效性

本章(第8章)討論了方法設計的幾個方面:如何處理引數和返回值,如何設計方法簽名以及如何記載方法文件。 本章中的大部分內容適用於構造方法和其他普通方法。 與第4章一樣,本章重點關注可用性,健壯性和靈活性上。

大多數方法和構造方法對可以將哪些值傳遞到其對應引數中有一些限制。 例如,索引值必須是非負數,物件引用必須為非null。 你應該清楚地在文件中記載所有這些限制,並在方法主體的開頭用檢查來強制執行。 應該嘗試在錯誤發生後儘快檢測到錯誤,這是一般原則的特殊情況。 如果不這樣做,則不太可能檢測到錯誤,並且一旦檢測到錯誤就更難確定錯誤的來源。

如果將無效引數值傳遞給方法,並且該方法在執行之前檢查其引數,則它丟擲適當的異常然後快速且清楚地以失敗結束。 如果該方法無法檢查其引數,可能會發生一些事情。 在處理過程中,該方法可能會出現令人困惑的異常。 更糟糕的是,該方法可以正常返回,但默默地計算錯誤的結果。 最糟糕的是,該方法可以正常返回但是將某個物件置於受損狀態,在將來某個未確定的時間在程式碼中的某些不相關點處導致錯誤。 換句話說,驗證引數失敗可能導致違反故障原子性(failure atomicity )(條目 76)。

對於公共方法和受保護方法,請使用Java文件@throws註解來記在在違反引數值限制時將引發的異常(條目 74)。 通常,生成的異常是IllegalArgumentExceptionIndexOutOfBoundsExceptionNullPointerException(條目 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類中添加了範圍檢查工具。 此工具包含三個方法:checkFromIndexSizecheckFromToIndexcheckIndex。 此工具不如空檢查方法靈活。 它不允許指定自己的異常詳細訊息,它僅用於列表和陣列索引。 它不處理閉合範圍(包含兩個端點)。 但如果它能滿足你的需要,那就很方便了。

對於未匯出的方法,作為包的作者,控制呼叫方法的環境,這樣就可以並且應該確保只傳入有效的引數值。因此,非公共方法可以使用斷言檢查其引數,如下所示:

// 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)習慣用法將自然異常轉換為正確的異常。

不要從本條目中推斷出對引數的任意限制都是一件好事。 相反,你應該設計一些方法,使其儘可能通用。 假設方法可以對它接受的所有引數值做一些合理的操作,那麼對引數的限制越少越好。 但是,通常情況下,某些限制是正在實現的抽象所固有的。

總而言之,每次編寫方法或構造方法時,都應該考慮對其引數存在哪些限制。 應該記在這些限制,並在方法體的開頭使用顯式檢查來強制執行這些限制。 養成這樣做的習慣很重要。 在第一次有效性檢查失敗時,它所需要的少量工作將會得到對應的回報。