1. 引言
NullPointerException
應該是 Java 開發中最常出現的問題,也是 Java 程式設計師最容易犯的錯誤。雖然看起來是個小錯誤,但帶來的影響卻不小,Tony Hoare(null 引用的發明者)在 2009 年說過 NPE 大約給企業造成數十億美元的損失。在這工作半年內,我就踩了好幾次 NPE 的坑。舉個例子,我需要在原有邏輯上加一段程式碼,而新加的程式碼報錯丟擲了 NPE,同時又沒做異常處理,就直接導致後面的邏輯不執行了,影響了整個原有邏輯,太恐怖了。所以大家一定要小心避開 NPE 這個坑。
本文將會從以下兩個方面說起:
- 發生 NPE 的可能情況
- 避開 NPE 的建議
2. 發生 NPE 的可能情況
首先我們需要清楚 NPE 是怎麼發生的。
String s;
String[] ss;
複製程式碼
當宣告一個引用變數時,若未指定其指向的內容,Java 會將其預設指向 null,一個空地址,意味著“什麼都沒有指向”。後續若也沒有為該變數賦值,則當使用這個變數裡的內容時,便會丟擲 NPE。
例如通過.
去訪問方法或者變數,[]
去訪問陣列插槽:
System.out.println(s.length());
System.out.println(ss[0]);
複製程式碼
以下是 NPE 的 Javadoc 概述的 6 個可能發生情況:
-
在空物件上呼叫例項方法。對空物件呼叫靜態方法或類方法時,不會報 NPE,因為靜態方法不需要例項來呼叫任何方法;
-
訪問或更改空物件上的任何變數或欄位時;
-
丟擲異常時丟擲 null;
-
陣列為 null 時,訪問陣列長度;
-
陣列為 null 時,訪問或更改陣列的插槽;
-
對空物件進行同步或在同步塊內使用 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_LIST
、Collections.EMPTY_Set
、Collections.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. 參考
- Java Tips and Best practices to avoid NullPointerException in Java Applications
- 如何在 Java8 中風騷走位避開空指標異常
喜歡我文章的小夥伴,可以掃碼關注下我的公眾號:“草捏子”