好的習慣
什麼時候要考慮判空呢?最常見的就那麼三種情況
使用呼叫某個方法得到的返回值之前,方法的api說明中明確指出可能會返回空,或者api文件不靠譜。
使用傳入的引數前。
獲取到一個多層巢狀物件,使用內層物件之前(鏈式呼叫尤其要小心)。
如果不做良好的判空處理,NullPointerException就會發生,有的時候會引發很致命的故障。
除了上面三種情況,再根據我的經驗列舉一些發生NPE的常見情況:
OR對映的時候,一些預期中不會為空的資料變成了空,框架又沒有做防禦性處理。
常見的低階錯誤:呼叫toString(), compare()等方法不判空(統計出來的N多故障是toString()的時候出現的空,很低階,但是事實如此)。
非同步呼叫,結果值沒有返回時就使用。(看一下Future)
呼叫超時處理不當。
RPC的被呼叫方做了修改,沒通知呼叫方/RPC呼叫失敗(很多原因,如鑑權廢了,底層連結有問題,超時等)。
基本型別的包裹型例項如果被賦值為空,且被自動拆箱時。
syncronized了一個空物件。
那麼如何較好的處理null呢?
- 使用前判空。這是最常見的使用辦法。
if (obj != null){
//do something
}
複製程式碼
- 較好的程式設計方式是提前判斷錯誤(可以參考 Guard Clause 模式),這樣能夠消除過度巢狀的情況出現。
if(obj == null){
//錯誤處理,一般是返回約定的錯誤,或者拋Exception
}
複製程式碼
- 也**可以使用一些工具類減少程式碼量,讓程式設計模式更清晰**。例如google的Guava框架提供了Preconditions工具,來幫助程式設計師快速的做引數檢測,Preconditions裡有一個靜態方法checkNotNull,如果不為空,則返回被檢測的物件本身,如果被檢測的物件為空,則會丟擲NullPointerException。
@Test
public void testGuavaNotNull(){
Object obj = null;
String errorMessage = "obj is null";
Preconditions.checkNotNull(obj,errorMessage);
}
//一般的使用方式是這樣的 對於一個輸入引數或者呼叫其它方法返回的值 objToBeChecked
try{
...
// 異常訊息收集或構造
obj = Preconditions.checkNotNull(objToBeChecked,errorMessage);
...
}catch(NullPointerException npe){
// 異常處理
}
複製程式碼
Java基礎庫的框架中其實也提供了簡單的靜態方法 java.util.Objects.reqireNonNull(T obj),但是Guava框架的好處是,你可以構造具體的errorMessage傳遞給檢測函式,從而在異常被丟擲後,程式設計師可以得到更具體的異常資訊。
空處理常見的工具類還有Spring的ObjectUtils,Apache Common Lang的 ObjectUtils (這個工具類其實非常強大,裡邊函式的處理Null的思路也非常值得借鑑)等,舉一個例子。假設一個場景,需要多物件的判空邏輯,就可以使用工具類的執行緒函式,讓語義更清晰,減少錯誤。
//比較冗長
if(obj1 == null || obj2 == null || boj3 == null){
//do something
}
//ObjectUtils 的方式:語義直接,不易出錯
(if(ObjectUtils.anyNotNull(obj1,obj2,obj3))){
// do something
}
複製程式碼
也有框架提供了類似於Assert的 工具類,如Lombok 的 @NonNull註解,如果被註解的物件是空值,直接會丟擲NPE,用作對輸入引數的檢查,會讓程式碼變優雅不少,**語義也更清晰**。類似這樣的工具遵循了JSR305, 具體實現有很多,比如findbugs,SpotBugs,Spring,AndroidTookit等都提供了這樣的註解。註解可以非常方便的掛在方法、輸入引數上。
//Lombok的例子,如果 obj為null,直接丟擲NPE異常,
public void LombokNullCheck(@NonNull Object obj){
// 可以直接使用obj
}
複製程式碼
- Java8提供的改進,Java8提供了一個叫做Optional的型別,在實戰中非常實用,Optional型別和stream API一併使用的話,能讓空檢查變得更加優雅,特別是複雜巢狀物件的空檢查。但讀很多人的程式碼,發現他們並沒有習慣這樣使用。先上一段程式碼感受一下。
class Passenger{
private Seat seat;
private Cert cert;
Cert getCert(){
return cert;
}
...
}
class Cert{
private PersonalInfo pi;
PersonalInfo getPersonalInfo(){
return pi;
}
}
class PersonalInfo{
private String name;
String getName();{
return name;
}
}
複製程式碼
對於巢狀比較深的類,下面這樣的程式碼太常見了,大段的&&條件判斷非常容易出錯,**程式碼可讀性也非常差。**
Passenger passenger = SomeMehtod.getPassenger();
if(passenger != null && passenger.getCert() != null
&& passenger.getCert().getPersonalInfo != null){
return passenger.getCert().getPersonalInfo().getName();
}
else return "default name";
//有更差的實踐是寫成下面的多重巢狀if模式,這樣在真實情況下很容易縮排七八層,甚至十幾層,程式碼可讀性基本上就沒了。
if(passenger != null){
if (passenger.getCert() != null){
if(passenger.getCert().getPersonalInfo() != null){
return passenger.getCert().getPersonalInfo().getName();
}else return "default name"
}
}
//還有更差的實踐,比如生成很多隻用一次的中間物件。對了,就是把 Cert,PersonalInfo 再都new出來。程式碼太難看,就不補全了。
複製程式碼
使用Optional 配合lambda表示式的效果,見下面程式碼,是不是非常簡潔清晰了?
Passenger passenger = SomeMehtod.getPassenger();
return Optional.ofNullable(passenger)
.map(Passenger::getCert)
.map(Cert::getPersonalInfo)
.map(PersonalInfo::getName)
.orElse("default name");
複製程式碼
如果,需要丟擲一個空指標異常而不是返回預設值,可以寫成下面這樣。
Passenger passenger = SomeMehtod.getPassenger();
return Optional.ofNullable(passenger)
.map(Passenger::getCert)
.map(Cert::getPersonalInfo)
.map(PersonalInfo::getName)
.orElesThrow(NullPointerException::new)
複製程式碼
對於巢狀類的空判斷,使用Optional比傳統的層層剝皮判斷要好很多。其他新一些的語言如Swift,Kotlin都提供了內建語言支援,會更加優雅,程式碼量也會少很多。需要特別說明的是,最好通讀Java8的Stream API文件,瞭解蘭布達表示式的正確使用方式,才能以正確的方式開啟。如果不結合Stream API來看,Optional反而讓程式碼變得更冗餘了。
作為呼叫方的義務
儘量不要把null當做一個引數傳遞。這個其實很好了解,當你傳入一個null的時候,如果不知道被調方法的具體實現,你不知道會觸發什麼。假如被呼叫函式沒做空處理,假如這個null又被傳遞了出去,影響就不可控了,除非你知道被呼叫方法的所有具體實現。
作為API提供方的義務
對傳入的引數做判空處理;良好的API文件標明傳入空值的後果和什麼情況下會丟擲NPE或者包裝了的其它異常。使用@NonNull 這種assert工具;不要繼續傳遞傳入的空值。丟擲NPE比返回一個null要好的多(如果考慮效能影響則另當別論),儘量不返回null,如果必須要,文件一定要說明。
單元測試的防護網很可能救你一命。一些程式碼的生命週期很長。有些地方的判空處理如果有對應的單元測試覆蓋這部分邏輯,當其他維護者(非常有可能是其他維護者)不小心修改了這部分邏輯,對應的單元測試很可能會救系統一命。它會提醒新來的維護者:你踩了個地雷,好好看看是不是應該這樣。我見過好幾次這樣的救命案例了。
NPE一定要嚴防死守麼? 答案是否定的。識別異常的含義,並正確利用,是一個程式設計師的素養。
一個複雜巢狀物件中,我們要對所有的欄位判空麼?那豈不是要寫死人了。 文章最初的那個例子就是這樣:如果對所有欄位全寫判空,程式碼量會很感人,讀起來會更感人。其實這個問題也是有解的,如果你只關注部分欄位,就只對它們判空並讀取,別的欄位別碰。如果必須要碰,在外層做catch(會影響效能,別在效能關鍵點做)。另外,讀取的資料來源其實應該有很好的註釋,並應該有nullable 的assert,修改資料的人應該仔細讀這些註釋,並不去破壞規則,畢竟軟體是多人協作。大家都遵守約定,才能降低協作中產生的錯誤概率。
再聊兩句防禦性程式設計: 防禦性程式設計上世紀80年代就提出來了,其核心觀念是:“預防你認為不可能發生的,時間長了,它一定會發生。”,防禦性程式設計裡有好多套路:永遠不要相信使用者輸入,呼叫時永遠做異常判斷等等等等。有一些防禦性程式設計的意識是一件非常好的事兒,能防止很多低階錯誤產生。但是一些不必要的嚴防死守,會讓程式碼變得很醜陋和複雜。很多事兒,過猶不及。
碼字不易,如有建議請掃碼