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