Java null最佳實踐

JavaDog發表於2019-01-29

好的習慣

什麼時候要考慮判空呢?最常見的就那麼三種情況

  • 使用呼叫某個方法得到的返回值之前,方法的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年代就提出來了,其核心觀念是:“預防你認為不可能發生的,時間長了,它一定會發生。”,防禦性程式設計裡有好多套路:永遠不要相信使用者輸入,呼叫時永遠做異常判斷等等等等。有一些防禦性程式設計的意識是一件非常好的事兒,能防止很多低階錯誤產生。但是一些不必要的嚴防死守,會讓程式碼變得很醜陋和複雜。很多事兒,過猶不及。

碼字不易,如有建議請掃碼Java null最佳實踐


相關文章