天天看點

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。

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

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

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