阿里P8架構師淺析如何避免在Java中檢查Null語句

點關注,不迷路;持續更新Java架構相關技術及資訊熱文!!!

一.概述


一般,在Java代碼中處理null變量、引用和集合很棘手。它們不只難以識別,並且處理起來也很複雜。事實上,在編譯時沒法識別處理null的任何錯誤,會致使運行時NullPointerException。在本教程中,咱們將瞭解在Java中檢查null的必要性以及幫助咱們避免在代碼中進行空檢查的各類替代方法。程序員

二.什麼是NullPointerException?


根據 Javadoc for NullPointerException,當應用程序在須要對象的狀況下嘗試使用null時拋出它,例如:sql

  • 調用null對象的實例方法
  • 訪問或修改空對象的字段
  • 取null的長度,就好像它是一個數組同樣
  • 訪問或修改null的插槽,就像它是一個數組同樣
  • 拋出null就好像它是一個Throwable值

讓咱們快速查看致使此異常的Java代碼的幾個示例:apache

publicvoid doSomething(){
    String result = doSomethingElse();
    if(result.equalsIgnoreCase("Success"))
    // success
}
}
privateString doSomethingElse(){
returnnull;
}

在這裏,咱們嘗試調用null引用的方法調用。這將致使NullPointerException。另外一個常見示例是,若是咱們嘗試訪問空數組:數組

publicstaticvoid main(String[] args){
    findMax(null);
}
privatestaticvoid findMax(int[] arr){
    int max = arr[0];
    //check other elements in loop
}

這會在第6行致使 NullPointerException。所以,訪問空 對象的任何字段,方法或索引會致使 NullPointerException,如上面的示例所示。避免 NullPointerException的 常見方法是檢查 null:安全

publicvoid doSomething(){
    String result = doSomethingElse();
    if(result !=null&& result.equalsIgnoreCase("Success")){
        // success
    } else
    // failure
}
privateString doSomethingElse(){
    returnnull;
}

在現實世界中,程序員發現很難識別哪些對象能夠爲 null。積極安全的策略多是爲每一個對象檢查 null。可是,這會致使大量冗餘空值檢查,並使咱們的代碼可讀性下降。在接下來的幾節中,咱們將介紹Java中的一些備選方案,以免這種冗餘。架構

三.經過API約定處理null


如上一節所述,訪問null對象的方法或變量會致使NullPointerException。 咱們還討論了在訪問對象以前對對象進行空 檢查能夠消除NullPointerException的可能性。可是,一般有API能夠處理空值。例如:併發

publicvoid print(Object param){
    System.out.println("Printing "+ param);
}
publicObject process()throwsException{
    Object result = doSomething();
    if(result ==null){
        thrownewException("Processing fail. Got a null response");
    } else{
        return result;
    }
}

在 print()方法調用將只打印 null,但不會拋出異常。一樣, process()永遠不會在其響應中返回 null。它反而拋出異常。所以對於訪問上述API的客戶端代碼,不須要進行空檢查。可是此類API必須在約定中明確說明。API發佈此類約定的常見位置是JavaDoc。可是,這並未明確指出API約定,所以依賴於客戶端代碼開發人員來確保其合規性。在下一節中,咱們將看到一些IDE和其餘開發工具如何幫助開發人員解決這個問題。app

四.自動化API約定


4.1.使用靜態代碼分析

靜態代碼分析工具備助於提升代碼質量。一些這樣的工具也容許開發人員維護null約定(Null Contracts)。一個例子是 FindBugs。 FindBugs經過 @Nullable和 @NonNull註解幫助管理null約定。咱們能夠在任何方法,字段,局部變量或參數上使用這些註釋。這使得對客戶端代碼明確指出註釋類型是否爲 null。咱們來看一個例子:分佈式

publicvoid accept(@NonnullObject param){
    System.out.println(param.toString());
}

在這裏, @NonNull清楚地代表參數不能爲 null。若是客戶端代碼在不檢查 null參數的狀況下調用此方法 ,則 FindBugs將在編譯時生成警告。函數

4.2.使用靜態代碼分析

開發人員一般依靠IDE來編寫Java代碼。使用代碼自動補全和有用警告等功能,例如可能沒有聲明變量,在很大程度上對編碼有幫助。一些IDE還容許開發人員管理API約定(API Contracts),從而消除對靜態代碼分析工具的需求。IntelliJ IDEA提供 @NonNull和 @Nullable註解。要在IntelliJ中添加對這些註釋的支持,咱們必須添加如下Maven依賴項:

<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>16.0.2</version>
</dependency>

如今,若是沒有對 Null進行檢查,IntelliJ將生成警告,就像咱們在上一個示例中同樣。IntelliJ還提供了用於處理複雜API約束的Contract註釋。

五. 斷言


到目前爲止,咱們只討論過從客戶端代碼中去除空檢查的必要性。可是,這不多適用於實際應用。如今,假設咱們正在使用一個不能接受空參數的API,或者能夠返回必須由客戶端處理的空響應。這代表咱們須要檢查參數或空值的響應。這裏,咱們可使用Java Assertions代替傳統的 null檢查條件語句:

publicvoid accept(Object param){
    assert param !=null;
    doSomething(param);
}

在第2行中,咱們檢查null參數。若是啓用了斷言,則會致使 AssertionError。儘管這是斷言非空參數等前置條件的好方法,但這種方法主要存在兩個問題:

  • 一般在JVM中禁用斷言
  • 一個虛假的聲明將致使在未經檢查的錯誤沒法恢復

所以,建議程序員不要使用斷言來檢查條件。在如下部分中,咱們將討論處理null檢查的其餘方法

六.經過編碼實踐避免NULL檢查


6.1.前提條件

編寫早期失敗的代碼一般是一種很好的作法。所以,若是一個API不容許接受有多個參數爲空,更好地方法是預先檢查API中的每個非空參數。

例如,讓咱們看看兩個方法:一個早期失敗,另外一個不失敗:

publicvoid goodAccept(String one,String two,String three){
    if(one ==null|| two ==null|| three ==null){
        thrownewIllegalArgumentException();
    }
    process(one);
    process(two);
    process(three);
}
publicvoid badAccept(String one,String two,String three){
    if(one ==null){
        thrownewIllegalArgumentException();
    } else{
        process(one);
    }
    if(two ==null){
        thrownewIllegalArgumentException();
    } else{
        process(two);
    }
    if(three ==null){
        thrownewIllegalArgumentException();
    } else{
        process(three);
    }
}

顯然,咱們應該更喜歡 goodAccept()而不是 badAccept()。做爲替代方案,咱們也可使用Guava的前置條件來驗證API參數。

6.2.使用原語而不是包裝類

因爲 null對於像int這樣的原語來講不是一個可接受的值,咱們應該儘量優先於它們的包裝對象,如 Integer。考慮一個對兩個整數求和的方法的兩個實現:

publicstaticint primitiveSum(int a,int b){
    return a + b;
}
publicstaticInteger wrapperSum(Integer a,Integer b){
    return a + b;
}

6.3.空集合

有時,咱們須要將一個集合做爲方法的響應返回。對於這樣的方法,咱們應該老是嘗試返回一個空集合而不是 null

publicList<String> names(){
    if(userExists()){
        returnStream.of(readName()).collect(Collectors.toList());
    } else{
        returnCollections.emptyList();
    }
}

所以,咱們在調用此方法時避免了客戶端執行空檢查的須要。

七.使用 Objects


Java 7引入了新的Objects API。此API有幾個靜態 實用程序方法,能夠消除大量冗餘代碼。讓咱們看看一個這樣的方法, requireNonNull():

publicvoid accept(Object param){
    Objects.requireNonNull(param);
    // doSomething()
}

如今,讓咱們測試 accept方法:

assertThrows(NullPointerException.class,()-> accept(null));

所以,若是將null 做爲參數傳遞,則 accept()會拋出 NullPointerException。此類還具備 isNull()和 nonNull()方法,可用做謂詞來檢查對象是否爲null。

八.使用Optional


Java8在該語言中引入了一個新的 OptionalAPI。與null相比,這爲處理可選值提供了更好的約定。讓咱們看看 Optional如何消除對空檢查的需求:

publicOptional<Object> process(Boolean processed){
    String response = doSomething(processed);
    if(response ==null){
        returnOptional.empty();
    }
    returnOptional.of(response);
}
privateString doSomething(Boolean processed){
    if(processed){
        return"passed";
    } else{
        returnnull;
    }
}

經過返回一個 Optional,如上所示,該 process()方法使得明確告訴調用者,響應多是Null,而且必須在編譯時處理。 這顯然消除了客戶端代碼中對空檢查的需求。可使用 OptionalAPI的聲明性樣式以不一樣方式處理空響應:

assertThrows(Exception.class,()-> process(false).orElseThrow(()->newException()));

此外,它還爲API開發人員提供了一個更好的約定,以向客戶端代表API能夠返回空響應。雖然咱們不須要對此API的調用者進行空檢查,但咱們使用它來返回空響應。爲避免這種狀況, Optional提供了一個 ofNullable方法,該方法返回具備指定值的 Optional,若是值爲 null,則返回 empty:

publicOptional<Object> process(Boolean processed){
    String response = doSomething(processed);
    returnOptional.ofNullable(response);
}

九.庫


9.1.使用Lombok

Lombok是一個很棒的庫,能夠減小項目中樣板代碼的數量。它附帶了一組註釋,取代了咱們常常在Java應用程序中編寫的代碼的常見部分,例如getter,setter和toString(),僅舉幾例。

另外一個註釋是 @NonNull。 所以,若是項目已經使用Lombok來消除樣板代碼,則 @NonNull能夠代替做爲空檢查。

在繼續查看一些示例以前,添加一個Maven依賴項引入Lombok:

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.6</version>
</dependency>

如今,咱們能夠在須要進行空檢查的地方 使用 @NonNull:

publicvoid accept(@NonNullObject param){
    System.out.println(param);
}

所以,咱們只是註解了須要進行null檢查的對象,而且Lombok生成了已編譯的類:

publicvoid accept(@NonNullObject param){
    if(param ==null){
        thrownewNullPointerException("param");
    } else{
        System.out.println(param);
    }
}

若是 param爲null,則此方法拋出 NullPointerException。該方法必須在其約定中明確說明,而且客戶端代碼必須處理異常。

9.2.使用StringUtils

通常來講,字符串驗證包括除空值檢查空值。所以,常見的驗證聲明是:

publicvoid accept(String param){
    if(null!= param &&!param.isEmpty())
    System.out.println(param);
}

若是咱們必須處理不少 String類型,這很快就會變得多餘。這就是 StringUtils派上用場的地方。在咱們看到這個動做以前,讓咱們爲commons-lang3添加一個Maven依賴項:

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>

如今讓咱們用 StringUtils重構上面的代碼 :

publicvoid accept(String param){
    if(StringUtils.isNotEmpty(param))
    System.out.println(param);
}

所以,咱們使用靜態實用程序方法 isNotEmpty()替換了 null或空檢查。此API提供了其它強大而實用方法來處理常見的String函數。

十. 結論


在本文中,咱們研究了發生 NullPointerException的各類緣由以及難以識別的緣由。而後,咱們使用了各類方法來避免代碼中的冗餘,以及對使用參數,返回類型和其餘變量進行空檢查。全部示例均可以在GitHub上找到。

讀者福利

分享免費學習資料

針對於Java程序員,我這邊準備免費的Java架構學習資料(裏面有高可用、高併發、高性能及分佈式、Jvm性能調優、MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)

爲何某些人會一直比你優秀,是由於他自己就很優秀還一直在持續努力變得更優秀,而你是否是還在知足於現狀心裏在竊喜!但願讀到這的您能點個小贊和關注下我,之後還會更新技術乾貨,謝謝您的支持!

資料領取方式:加入Java技術交流羣963944895點擊加入羣聊,私信管理員便可免費領取

相關文章
相關標籤/搜索