- <strong>
- <strong>
- <strong>
- <strong>
1. 引言
NullPointerException應該是 Java 開發中最常出現的問題,也是 Java 程序員最容易犯的錯誤。雖然看起來是個小錯誤,但帶來的影響卻不小,Tony Hoare(null 引用的發明者)在 2009 年說過 NPE 大約給企業造成數十億美元的損失。在這工作半年內,我就踩了好幾次 NPE 的坑。舉個例子,我需要在原有邏輯上加一段代碼,而新加的代碼報錯拋出了 NPE,同時又沒做異常處理,就直接導致後面的邏輯不運行了,影響了整個原有邏輯,太恐怖了。所以大家一定要小心避開 NPE 這個坑。
本文將會從以下兩個方面說起:
- 發生 NPE 的可能情況
- 避開 NPE 的建議
2. 發生 NPE 的可能情況
首先我們需要清楚 NPE 是怎麼發生的。
<code>String s;String[] ss;/<code>
當聲明一個引用變量時,若未指定其指向的內容,Java 會將其默認指向 null,一個空地址,意味著“什麼都沒有指向”。後續若也沒有為該變量賦值,則當使用這個變量裡的內容時,便會拋出 NPE。
例如通過.去訪問方法或者變量,[]去訪問數組插槽:
<code>System.out.println(s.length());System.out.println(ss[0]);/<code>
以下是 NPE 的 Javadoc 概述的 6 個可能發生情況:
- 在空對象上調用實例方法。對空對象調用靜態方法或類方法時,不會報 NPE,因為靜態方法不需要實例來調用任何方法;
- 訪問或更改空對象上的任何變量或字段時;
- 拋出異常時拋出 null;
- 數組為 null 時,訪問數組長度;
- 數組為 null 時,訪問或更改數組的插槽;
- 對空對象進行同步或在同步塊內使用 null。
3. 避開 NPE 的建議
這節將介紹如何在開發過程中避開 NPE 的一些建議。
(1)儘量避免在未知對象上調用 equals() 方法和 equalsIgnoreCase() 方法,而是在已知的字符串常量上調用
由於 equals() 和 equalsIgnoreCase() 具有對稱性,所以可以直接翻轉,這是很容易實現的。
<code>Object unknowObject = null;if (unknowObject.equals("knowObject")) { System.out.println("如果 unknowObject 是 null,則會拋出 NPE");}if ("knowObject".equals(unknowObject)) { System.out.println("避免 NPE");}/<code>
(2)避免使用 toString(),而是 String.valueOf()
這是因為 String.valueOf() 中做了非空校驗,同樣裡面也調用了對象的 toString()方法,所以結果是相同的。
<code>Object unknowObject = null;System.out.println(unknowObject.toString());System.out.println(String.valueOf(unknowObject));/<code>
(3)使用 null 安全的方法和庫
開源庫的方法通常都了非空校驗,例如 Apache common 庫中的 StringUtils 工具類中的 isBlank()、isNumeric() 等方法,使用時不必擔心 NPE。那我們在使用第三方庫時,一定要了解它是否是 null 安全的,如果不是,則需要我們自己做好非空校驗。
<code>System.out.println(StringUtils.isBlank(null));System.out.println(StringUtils.isNumeric(null));/<code>
(4)當方法返回集合或數組時,避免返回 null,而應是空集合或空數組
返回空集合或空數組時,可以保證調用方法(如size()、length())不會出現 NPE。而且Collections 類中提供了方便的空 List、Set和Map,Collections.EMPTY_LIST、Collections.EMPTY_Set、Collections.EMPTY_MAP。
<code>public List fun(Customer customer){ List result = Collections.EMPTY_LIST; return result;}/<code>
(5)使用 @NotNull 和 @Nullable 註解
- @NonNull可以標註在方法、字段、參數之上,表示對應的值不可以為空
- @Nullable可以標註在方法、字段、參數之上,表示對應的值可以為空
以上兩個註解在程序運行的過程中不會起任何作用,只會在IDE、編譯器、FindBugs檢查、生成文檔的時候提示。
有好幾種 @NotNull 和 @Nullable,我還沒能搞明白,具體怎麼使用我先不講了。但即使不談檢測,單純作為標識也是能夠起到文檔的作用。
(6)避免不必要的裝箱拆箱
如果包裝對象為 null,在拆箱時容易發生 NPE。
<code>Integer integer = null;int i = integer;System.out.println(i);/<code>
(7)定義合理的默認值
定義成員變量時提供合理的默認值。
<code>public class Main { private List<string> list = new ArrayList<>(); private String s = "";}/<string>/<code>
(8)使用空對象模式
空對象是設計的一種特殊實例,為方法提供默認的行為,例如 Collections中的 EMPTY_List,我們仍能使用它的 size(),會返回 0,而不會拋出 NPE。
再舉個 Jackson 中的例子,當子節點不存在時,path()會返回一個 MissingNode 對象,當調用 MissingNode 對象的 path() 方法是將繼續返回 MissingNode。這樣的鏈式調用將不會拋出 NPE。最後返回後,用戶只需檢查結果是否為 MissingNode 就能判斷是不是找到了。
<code>JsonNode child = root.path("a").path("b");if (child.isMissingNode()) { //...}/<code>
(9)Optional
Optional 是 Java8 的一個新特性,可以為 null 的容器對象。若值存在,不為 null,則 isPresent()方法會返回 true,調用 get()方法可返回該對象。它所起到的作用是避免我們顯示的進行空值校驗。
舉一個常見的空值校驗示例:
<code>// 最外層public class Outer { Nested nested; Nested getNested() { return nested; }}/<code>
<code>// 第二層public class Nested { Inner inner; Inner getInner() { return inner; }}/<code>
<code>// 最底層public class Inner { String foo; String getFoo() { return foo; }}/<code>
我們通過 Outer 對象訪問 Inner 中的 foo 屬性,若加空值校驗的話,代碼如下:
<code>Outer outer = new Outer();if (outer != null) { if (outer.nested != null) { if (outer.nested.inner != null) { System.out.println(outer.nested.inner.foo); } }}/<code>
這種嵌套式的判斷語句在空值校驗中很常見。而使用 Optional 再結合 Java8 的特性 Lambda 表達式、流處理,可以採用鏈式操作,更為簡潔。
<code>Optional.of(new Outer()) .map(Outer::getNested) .map(Nested::getInner) .map(Inner::getFoo) .ifPresent(System.out::println);/<code>
Optional.of() 方法可以返回一個 Optional<outer> 的對象,並將 Outer 對象放在容器內,Optinal.map()方法中,會通過 isPresent() 方法判斷是否為 null,如果為 null,將返回 Optional<outer> 類型的空對象,不影響後續的鏈式調用。是不是很眼熟,這和我們在第 8 點說的空對象模式類似,在 Optional 的實現中也採用了這種模式。/<outer>/<outer>
(10)細心
嘿嘿,湊個第十點吧。
最後祝大家成功避開 NullPointerException,有什麼其他的好建議,歡迎留言交流!
作者:草捏子
原文鏈接:https://juejin.im/post/5e510b85518825496e784b4e
閱讀更多 追逐仰望星空 的文章