阿里Java架構師教你寫代碼-如何校驗參數?
1 參數校驗的意義
大多數方法會限制傳遞給它們的參數值。常見的比如,索引值非負,引用非空。作為優雅的開發者,應做到:
- 在Java Doc中清楚地記錄這些限制,并在方法體開頭校驗
- 在錯誤發生后盡快找到。若不這樣做,就不太可能檢測到錯誤,而且即使檢測到錯誤,確定其源頭也很難
若一個無效參數被傳遞給一個方法,若該方法
- 校驗參數,方法將迅速失敗,并拋異常
- 未校驗參數,可能會在方法執行過程中發生如下情形:
- 莫名其妙的異常而失敗
- 正常返回,但會暗中計算錯誤結果
- 正常返回,但會使某對象處于隱患狀態,可能在未來某不確定時間在某不相關代碼點報錯。
總之,若不校驗參數,可能會違反失敗原子性。
對public、protected方法,要在方法說明使用 Javadoc 的 @throws 標簽說明,若違反參數值限制時會拋出的異常。通常為 IllegalArgumentException、IndexOutOfBoundsException 或 NullPointerException。一旦在文檔中記錄了參數限制,并且記錄違反這些限制將引發的異常,強加這些限制就很簡單了。
看案例:
文檔注釋并沒說「若 m 為空,mod 將拋NPE」,然而方法確實做了,只是作為調用 m.signum() 的副產物。該異常記錄在外圍 BigInterger 類級別的文檔注釋。類級別注釋適用于類的所有public方法中的所有參數。可以避免在每個方法上分別記錄每個 NullPointerException 而造成雜糅。
可與 @Nullable 或類似注解協作,指示某參數可能為 null,但這種做法并非標準,而且使用了多個注解。
2 最佳實踐
Java 7 提供 Objects.requireNonNull 不再需手動執行空檢查。
如果愿意,還可自定義異常詳情。該方法返回其輸入,所以使用一個值的同時可執行判空:
- // Java 內置的判空功能
- this.strategy = Objects.requireNonNull(strategy, "strategy");
也可以忽略返回值并使用 Objects.requireNonNull 作為一個獨立判空方法。
3 邊界檢查
在 Java 9 中,邊界檢查功能被添加到 java.util.Objects。該功能由三個方法組成:
checkFromIndexSize
checkFromToIndex
checkIndex
該套工具不如判空方法靈活。它不允許自定義異常詳細信息,僅適用于 List 和數組索引,且不處理封閉范圍(包含兩個端點)。
4 斷言
對于未暴露的方法,作為包開發者,你應該控制方法在何時能被調用,因此你可以并且也應該確保只傳入有效參數值。因此,非public方法可使用斷言檢查入參:
從本質上說,這些斷言是在聲稱被斷言的條件為 true,而不管客戶端如何調用。與普通校驗不同的是:
- 若斷言失敗,會拋 AssertionError
- 若斷言沒有作用,本質上不存在成本,除非通過將 -ea或 -enableassertion標識傳遞給 java 命令來啟用它們
靜態工廠方法
尤其應檢查那些尚未由方法調用,而是存起供日后使用的參數的有效性。例如靜態工廠方法,它接受 int 數組并返回數組的 List 視圖。若客戶端傳入 null,將拋 NullPointerException,因為該方法具有顯式檢查(調用 Objects.requireNonNull)。如果省略檢查,該將返回對新創建的 List 實例的引用,該實例將在客戶端試圖使用它時拋出 NullPointerException。到那時,List 實例的起源很難確定,使調試變得復雜。
構造器就是一種特殊情況。務必檢查構造器入參有效性,避免構造生成實例對象時,違背對象的不變性。
例外
在執行方法前,應顯式檢查參數,也有例外 - 有效性檢查成本較高或不切實際,或檢查在計算過程中隱式執行了。
例如,一個為對象 List 排序的方法,比如 Collections.sort(List)。List 中的所有對象必須相互比較。在對 List 排序的過程中,List 中的每個對象都會與列表中的其他對象進行比較。如果對象不能相互比較,將拋出 ClassCastException,這正是 sort 方法應該做的。因此,沒有必要預先檢查列表中的元素是否具有可比性。但不加區別地依賴隱式有效性檢查可能導致失敗原子性的丟失。
有時,計算任務會隱式地執行所需的有效性檢查,但如果檢查失敗,則拋出錯誤的異常。即計算任務由于無效參數值所拋異常,與文檔中記錄的方法要拋出的異常不匹配。此時應該使用異常轉換將計算任務拋出的異常轉換為正確的異常。
5 總結
請勿從本文自以為對參數的限制永遠都是好事。我們追求的是通用又實用的方法設計。若該方法可對它所接受的所有參數值進行合理的處理,那么對參數所加限制越少越好。
建議你每次編寫方法前,考慮清楚參數存在哪些限制。在文檔中記錄這些限制并在方法主體的開頭顯式檢查。養成這樣的習慣!這一少量工作將在校驗出現失敗時給你一片春光!
參考
《阿里 Java 開發手冊》
《重構》
《Effective Java》