如何避免在java中檢查null語句(多種解決方案)

1. 概述

通常,null的變量、引用和集合在Java代碼中很難處理。它們不僅很難辯別,而且處理起來也很複雜.

事實上,在編譯時無法識別處理null的任何錯誤,並在運行時導致NullPointerException異常.

在本教程中,我們將瞭解在Java中檢查處理null的必要性,以及幫助我們避免在代碼中檢查處理null的各種替代方法.

2. 什麼是NullPointerException(NPE)?

根據Javadoc對於NullPointerException的定義,當應用程序在需要對象的地方對象爲null時被拋出,例如:

1. 調用null對象的實例方法
2. 訪問或修改null對象的字段
3. 當它是一個數組時取null的長度
4. 訪問或修改null[]
5. 像拋出Throwable一樣拋出null

    讓我們快速看幾個導致這種異常的Java代碼示例:
示例1:

public void doSomething() {
    String result = doSomethingElse();
    if (result.equalsIgnoreCase("Success")) 
        // 處理邏輯
    }
}
 
private String doSomethingElse() {
    return null;
}

這裏的result爲null,在第3行中拋出NPE異常

示例2:

public void doSomething() {
    User user = getUser();
    if ("konastin".equalsIgnoreCase(user.getUsername())) 
        // 處理邏輯
    }
}
 
private User getUser() {
    return null;
}

第二行中的我們在獲取user對象可能會獲取null,第三行中拋出NPE異常

示例3:

public static void main(String[] args) {
    findMax(null);
}
 
private static void findMax(int[] arr) {
    int max = arr[0];
    //check other elements in loop
}

這個例子第6行中拋出NPE異常

因此,訪問null對象的任何字段、方法或索引都會導致NullPointerException,從上面的示例中看到這些.

避免NullPointerException的一種常見方法是檢查null:

public void doSomething() {
    User user = getUser();
    if (user!=null&&"konastin".equalsIgnoreCase(user.getUsername())) 
        // 處理邏輯
    }
}
 
private User getUser() {
    return null;
}

這裏第三行進行null檢查.

在真是開發中,程序員很難確定哪些對象爲null.**一個非常安全的策略是爲每個對象檢查null.然而,這將導致大量冗餘的null檢查,並降低代碼的可讀性.**下面我們將通過Java中的一些替代方法來避免這種冗餘.

3. 通過API規則來提示NullPointerException可能出現的情況

正如上一節所討論的,訪問null對象的方法或變量會導致NullPointerException。我們還討論了在訪問對象之前對其進行null檢查可以消除NullPointerException的可能性

然而,通常有一些api可以處理空值。例如:

public void print(Object param) {
    System.out.println("Printing " + param);
}
 
public Object process() throws Exception {
    Object result = doSomething();
    if (result == null) {
        throw new Exception("Processing fail. Got a null response");
    } else {
        return result;
    }
}

print()方法調用只會打印“null”,但不會拋出異常。類似地,process()在其響應中永遠不會返回null。它會拋出異常。

因此,對於訪問上述api的客戶機代碼,不需要進行null檢查。

但是,這些api必須在它們的規則中明確表示。api發佈此類規則的常見位置是JavaDoc。

然而,這並沒有給出API規則的明確指示,因此依賴於客戶機代碼開發人員來確保其遵從。

4. 自動化API規則來提示NullPointerException可能出現的情況

4.1. 使用 Static Code Analysis

Static code analysis 工具可以極大地提高代碼質量. 一些這樣的工具還允許開發人員維護null規則。FindBugs就是一個例子。

FindBugs通過@Nullable和@NonNull註釋幫助管理空規則。我們可以對任何方法、字段、局部變量或參數使用這些註釋。這使得帶註釋的類型是否可以爲空對客戶機代碼顯式。我們來看一個例子:

public void accept(@Nonnull Object param) {
    System.out.println(param.toString());
}

這裏@NonNull清楚地表明,參數不能爲空。如果客戶機代碼調用此方法而沒有檢查null參數,FindBugs將在編譯時生成警告。

4.2. 使用 IDE 支持

開發人員通常依賴IDE編寫Java代碼。而諸如智能代碼完成和有用的警告(比如可能沒有分配變量)等特性,肯定在很大程度上有所幫助。

一些IDE還允許開發人員管理API規則,從而消除了對靜態代碼分析工具的需求。IntelliJ IDEA提供了@NonNull和@Nullable註解。要在IntelliJ中添加對這些註解的支持,我們必須添加以下Maven依賴:

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

IntelliJ將生成一個警告,如果缺少null檢查,就像我們上一個例子中那樣。

IntelliJ還爲處理複雜的API規則提供了一個規則註解。

5. 斷言功能(Assertions)

到目前爲止,我們只討論了從客戶機代碼中刪除null檢查的必要性。但是,這在實際應用中很少適用。

現在,假設我們使用的API不能接受null參數,或者能夠返回必須由客戶機處理的null響應。這就需要檢查參數或null值的響應。

在這裏,我們可以使用Java斷言代替傳統的null檢查條件語句:

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

第2行中,我們檢查一個空參數。如果啓用斷言,這將導致AssertionError。

雖然這是斷言非空參數等先決條件的好方法,但是這種方法有兩個主要問題:

JVM中通常禁用斷言
錯誤斷言會導致無法恢復的uncheck error

因此,不建議新手程序員使用斷言檢查條件。在接下來的幾節中,我們將討論處理null檢查的其他方法。

6. 通過代碼練習如何進行Null檢查

6.1. 提前處理

當我們開發程序時,可以通過提前進行NUll檢查,當真實出現null時,這個檢查會起作用,拋出NPE異常.
下面我會提供2個版本的代碼,第一個goodAccept是較好的風格

public void goodAccept(String one, String two, String three) {
    if (one == null || two == null || three == null) {
        throw new IllegalArgumentException();
    }
 
    process(one);
    process(two);
    process(three);
}
 
public void badAccept(String one, String two, String three) {
    if (one == null) {
        throw new IllegalArgumentException();
    } else {
        process(one);
    }
 
    if (two == null) {
        throw new IllegalArgumentException();
    } else {
        process(two);
    }
 
    if (three == null) {
        throw new IllegalArgumentException();
    } else {
        process(three);
    }
}

另外,我們還可以使用Guava的先決條件來驗證API參數。

6.2. 用包裝類替換基本類型

由於int基本類型不能接收null,所以我們應該傾向於使用它的包裝類Integer

如下思考兩個函數:

public static int primitiveSum(int a, int b) {
    return a + b;
}
 
public static Integer wrapperSum(Integer a, Integer b) {
    return a + b;
}

我們在調用端調用這兩個函數:

int sum = primitiveSum(null, 2);

這將導致編譯時錯誤,因爲null不是int的有效值。

當使用包裝類的函數(API)時,我們得到一個NullPointerException:
assertThrows(NullPointerException.class, () -> wrapperSum(null, 2));

6.3. 空集合

有時候,我們需要返回一個集合作爲方法的返回值。對於這樣的方法,我們應該返回一個空集合,而不是null:

public List<String> names() {
    if (userExists()) {
        return Stream.of(readName()).collect(Collectors.toList());
    } else {
        return Collections.emptyList();
    }
}

因此,我們避免了在調用此方法時的null檢查。

7. 使用 Objects

Java 7引入的新的Objects API。這個API有幾個靜態實用程序方法,可以幫我們刪除我們代碼中大量冗餘代碼(檢查null的代碼)。讓我們來看一個這樣的方法requireNonNull():

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

下面測試accept() 方法:

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

因此,如果null作爲參數傳遞,accept()將拋出NullPointerException。

該類還具有isNull()和nonNull()方法,可以將它們用作判斷,來檢查對象是否爲null。

8. 使用Optional

Java 8在該語言中引入的一個新的可選API。與null相比,Optional爲處理NPE提供了更好的規則。讓我們看看可選如何消除null檢查的需要:

public Optional<Object> process(boolean processed) {
    String response = doSomething(processed);
 
    if (response == null) {
        return Optional.empty();
    }
 
    return Optional.of(response);
}
 
private String doSomething(boolean processed) {
    if (processed) {
        return "passed";
    } else {
        return null;
    }
}

通過返回一個Optional, process()方法向調用者表明,響應可以爲空,並且必須在編譯時處理。

這顯著地消除了客戶端代碼中任何空檢查的需要。空響應可以使用可選API的聲明式以不同的方式處理:

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

此外,它還爲API開發人員提供了更好的規則,以便向客戶端表明API可以返回空響應。

雖然我們消除了對這個API調用者進行null檢查的需要,但是我們使用它來返回一個空響應。爲了避免這種情況, Optional 提供了一個ofNullable方法 ,這個方法會返回一個指定的值或者當出現null時返回empty:

public Optional<Object> process(boolean processed) {
    String response = doSomething(processed);
    return Optional.ofNullable(response);
}

9. 使用 Libraries

9.1. 使用Lombok

Lombok是一個很6的lib庫,可以減少項目中的代碼量。它附帶了一組註解,這些註解代替了我們在Java應用程序中經常編寫的常見代碼部分,例如getter、setter和toString()等。

另外它的其中的一個註解 @NonNull.因此,如果一個項目已經使用Lombok,可以使用@NonNull代替null檢查。

在我們繼續看一些例子之前,讓我們爲Lombok添加一個Maven依賴項:

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

在需要的地方使用@NonNull

public void accept(@NonNull Object param){
    System.out.println(param);
}

因此,我們只是註解了需要null檢查的對象,Lombok生成編譯後的類:

public void accept(@NonNull Object param) {
    if (param == null) {
        throw new NullPointerException("param");
    } else {
        System.out.println(param);
    }
}

如果param爲null,這個方法將拋出NullPointerException。方法必須在其按照@NonNull來顯式地實現這一點,客戶機代碼必須處理異常。

9.2. 使用StringUtils

通常,字符串驗證除了空值外,還包括對空值的檢查。因此,一個通用的驗證語句應該是:

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

如果我們必須處理許多字符串類型,這很快就會變得多餘。這就是StringUtils派上用場的地方。在看實際操作之前,讓我們爲commons-lang3添加一個Maven依賴

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

現在讓我們用StringUtils重構上面的代碼:

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

因此,我們用靜態實用程序方法isNotEmpty()替換了null或empty檢查。這個API提供了其他強大的實用程序方法來處理公共字符串函數.

10. 總結

在本文中,我們研究了NullPointerException的各種原因,以及它難以識別的原因。然後,我們學習了在真實開發時,如何處理NPE,以及如何避免null檢查,以此優化代碼.java新手很值得學習.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章