避開NullPointerException的10條建議

草捏子發表於2020-02-22

1. 引言

NullPointerException應該是 Java 開發中最常出現的問題,也是 Java 程式設計師最容易犯的錯誤。雖然看起來是個小錯誤,但帶來的影響卻不小,Tony Hoare(null 引用的發明者)在 2009 年說過 NPE 大約給企業造成數十億美元的損失。在這工作半年內,我就踩了好幾次 NPE 的坑。舉個例子,我需要在原有邏輯上加一段程式碼,而新加的程式碼報錯丟擲了 NPE,同時又沒做異常處理,就直接導致後面的邏輯不執行了,影響了整個原有邏輯,太恐怖了。所以大家一定要小心避開 NPE 這個坑。

本文將會從以下兩個方面說起:

  1. 發生 NPE 的可能情況
  2. 避開 NPE 的建議

2. 發生 NPE 的可能情況

首先我們需要清楚 NPE 是怎麼發生的。

String s;
String[] ss;
複製程式碼

當宣告一個引用變數時,若未指定其指向的內容,Java 會將其預設指向 null,一個空地址,意味著“什麼都沒有指向”。後續若也沒有為該變數賦值,則當使用這個變數裡的內容時,便會丟擲 NPE。

例如通過.去訪問方法或者變數,[]去訪問陣列插槽:

System.out.println(s.length());
System.out.println(ss[0]);
複製程式碼

以下是 NPE 的 Javadoc 概述的 6 個可能發生情況

  1. 在空物件上呼叫例項方法。對空物件呼叫靜態方法或類方法時,不會報 NPE,因為靜態方法不需要例項來呼叫任何方法;

  2. 訪問或更改空物件上的任何變數或欄位時

  3. 丟擲異常時丟擲 null

  4. 陣列為 null 時,訪問陣列長度

  5. 陣列為 null 時,訪問或更改陣列的插槽

  6. 對空物件進行同步或在同步塊內使用 null

3. 避開 NPE 的建議

這節將介紹如何在開發過程中避開 NPE 的一些建議。

(1)儘量避免在未知物件上呼叫 equals() 方法和 equalsIgnoreCase() 方法,而是在已知的字串常量上呼叫

由於 equals()equalsIgnoreCase() 具有對稱性,所以可以直接翻轉,這是很容易實現的。

Object unknowObject = null;
if (unknowObject.equals("knowObject")) {
    System.out.println("如果 unknowObject 是 null,則會丟擲 NPE");
}
if ("knowObject".equals(unknowObject)) {
    System.out.println("避免 NPE");
}
複製程式碼

(2)避免使用 toString(),而是 String.valueOf()

這是因為 String.valueOf() 中做了非空校驗,同樣裡面也呼叫了物件的 toString()方法,所以結果是相同的。

Object unknowObject = null;
System.out.println(unknowObject.toString());
System.out.println(String.valueOf(unknowObject));
複製程式碼

(3)使用 null 安全的方法和庫

開源庫的方法通常都了非空校驗,例如 Apache common 庫中的 StringUtils 工具類中的 isBlank()isNumeric() 等方法,使用時不必擔心 NPE。那我們在使用第三方庫時,一定要了解它是否是 null 安全的,如果不是,則需要我們自己做好非空校驗。

System.out.println(StringUtils.isBlank(null));
System.out.println(StringUtils.isNumeric(null));
複製程式碼

(4)當方法返回集合或陣列時,避免返回 null,而應是空集合或空陣列

返回空集合或空陣列時,可以保證呼叫方法(如size()length())不會出現 NPE。而且Collections 類中提供了方便的空 List、Set和Map,Collections.EMPTY_LISTCollections.EMPTY_SetCollections.EMPTY_MAP

public List fun(Customer customer){
   List result = Collections.EMPTY_LIST;
   return result;
}
複製程式碼

(5)使用 @NotNull 和 @Nullable 註解

  • @NonNull可以標註在方法、欄位、引數之上,表示對應的值不可以為空
  • @Nullable可以標註在方法、欄位、引數之上,表示對應的值可以為空

以上兩個註解在程式執行的過程中不會起任何作用,只會在IDE、編譯器、FindBugs檢查、生成文件的時候提示。

有好幾種 @NotNull@Nullable,我還沒能搞明白,具體怎麼使用我先不講了。但即使不談檢測,單純作為標識也是能夠起到文件的作用。

(6)避免不必要的裝箱拆箱

如果包裝物件為 null,在拆箱時容易發生 NPE。

Integer integer = null;
int i = integer;
System.out.println(i);
複製程式碼

(7)定義合理的預設值

定義成員變數時提供合理的預設值。

public class Main {
    private List<String> list = new ArrayList<>();
    private String s = "";
}
複製程式碼

(8)使用空物件模式

空物件是設計的一種特殊例項,為方法提供預設的行為,例如 Collections中的 EMPTY_List,我們仍能使用它的 size(),會返回 0,而不會丟擲 NPE。

再舉個 Jackson 中的例子,當子節點不存在時,path()會返回一個 MissingNode 物件,當呼叫 MissingNode 物件的 path() 方法是將繼續返回 MissingNode。這樣的鏈式呼叫將不會丟擲 NPE。最後返回後,使用者只需檢查結果是否為 MissingNode 就能判斷是不是找到了。

JsonNode child = root.path("a").path("b");
if (child.isMissingNode()) {
    //...
}
複製程式碼

(9)Optional

Optional 是 Java8 的一個新特性,可以為 null 的容器物件。若值存在,不為 null,則 isPresent()方法會返回 true,呼叫 get()方法可返回該物件。它所起到的作用是避免我們顯示的進行空值校驗。

舉一個常見的空值校驗示例:

// 最外層
public class Outer {
    Nested nested;
    Nested getNested() {
        return nested;
    }
}
複製程式碼
// 第二層
public class Nested {
    Inner inner;
    Inner getInner() {
        return inner;
    }
}
複製程式碼
// 最底層
public class Inner {
    String foo;
    String getFoo() {
        return foo;
    }
}
複製程式碼

我們通過 Outer 物件訪問 Inner 中的 foo 屬性,若加空值校驗的話,程式碼如下:

Outer outer = new Outer();
if (outer != null) {
    if (outer.nested != null) {
        if (outer.nested.inner != null) {
            System.out.println(outer.nested.inner.foo);
        }
    }
}
複製程式碼

這種巢狀式的判斷語句在空值校驗中很常見。而使用 Optional 再結合 Java8 的特性 Lambda 表示式、流處理,可以採用鏈式操作,更為簡潔。

Optional.of(new Outer())
    .map(Outer::getNested)
    .map(Nested::getInner)
    .map(Inner::getFoo)
    .ifPresent(System.out::println);
複製程式碼

Optional.of() 方法可以返回一個 Optional<Outer> 的物件,並將 Outer 物件放在容器內,Optinal.map()方法中,會通過 isPresent() 方法判斷是否為 null,如果為 null,將返回 Optional<Outer> 型別的空物件,不影響後續的鏈式呼叫。是不是很眼熟,這和我們在第 8 點說的空物件模式類似,在 Optional 的實現中也採用了這種模式。

(10)細心

嘿嘿,湊個第十點吧。

最後祝大家成功避開 NullPointerException,有什麼其他的好建議,歡迎留言交流!

4. 參考

  1. Java Tips and Best practices to avoid NullPointerException in Java Applications
  2. 如何在 Java8 中風騷走位避開空指標異常

喜歡我文章的小夥伴,可以掃碼關注下我的公眾號:“草捏子”

避開NullPointerException的10條建議

相關文章