下面這個報錯,相信沒有任何一個 Java 程式設計師沒有被它折磨過。我們對他的熟悉程度簡直超過了 Hello World。
何止是熟悉,那簡直是深惡痛絕,以至於我對它都產生了後遺症。
每當本地除錯出現這個錯誤的時候,都恨不得掐一下大腿,然後默默的對自己說:垃圾,還犯這麼愚蠢的錯誤呢?
不知道有多少同學和我一樣有這種感受呢?
回想起我之前接手的一個專案,線上出現了問題,當我到了伺服器一看日誌,只有幾個單詞,那就是 java.lang.NullPointerException,那一刻我是頭暈目眩,差點一頭撞在 27 寸的顯示器上。回想上一次出現這種症狀,還是幾年前擠早高峰的公交車,擠的我雙腳離地,外加有點低血糖。
當然主要問題並不是 NPE(NullPointerException),還是要仰仗前輩異常處理的“非常優秀”,異常包裹的嚴嚴實實的,只留了java.lang.NullPointerException這一點點資訊。
於是只能開啟程式碼,找到報錯的介面,一步步排查,滿眼看去,皆可空指標啊。從此之後,空指標異常給我留下了深深的陰影。
好在從 JDK 14之後,NPE 異常不再僅僅是簡單的這幾個單詞了,而會附帶更加具體的異常資訊,比如對一個賦值為 null 的字串求長度,能捕捉到下面這樣的異常資訊:
Cannot invoke "String.length()" because "s" is null
空指標的由來
要說空指標異常,那還不只是 Java 的問題,絕大多數語言都有這個問題,比如 C++、C#、Go,但是也有沒有這個問題,比如 Rust 。
空指標最早是程式設計界的鼻祖級人物 Tony Hoare 引入的,早在 1965年,他設計 ALGOL 60 語言的時候引入了Null 的設計,ALGOL 可謂是 C 語言的祖宗。ALGOL 中的 Null 被後來的眾多語言設計者引入,就包括前面提到的這些語言。
Tony Hoare 不僅發明了我們熟悉的 Null,還是令眾多演算法殘廢聞風喪膽的快速排序演算法(Quick Sort)的發明者,這個演算法也是當前世界上使用最廣泛的演算法之一。
空指標後遺症.png
2009年3月他在Qcon技術會議上發表了題為「Null引用:代價十億美元的錯誤」的演講,回憶自己1965年設計第一個全面的型別系統時,未能抵禦住誘惑,加入了Null引用,僅僅是因為實現起來非常容易。它後來成為許多程式設計語言的標準特性,導致了數不清的錯誤、漏洞和系統崩潰,可能在之後40年中造成了十億美元的損失
如何應對空指標
處理空指標有一些措施,我們常常稱之為「防禦式程式設計」,這個說法也很形象,你不防著它,它真的就上來傷害你。
1、主動檢查空指標,要使用一個變數之前,要檢查這個變數是不是空,不是空再操作,比如常用的對字串判空。
public static boolean isEmpty(CharSequence cs) { return cs == null || cs.length() == 0; }
對應的很多字串工具類都有 isEmpty、isNotEmpty、isNotBlank 這種方法。
同樣的,還有對於集合的判斷,好多工具包都有 CollectionUtil.isEmpty這樣的方法。
爲了避免空引用異常,有時候我們寫的程式碼可能想下面這個樣子,一步一判空。這樣可以提高程式碼的健壯性和可靠性,但是看上去並不是很美觀。
public static String getUserOrderDetail(Integer userId) { User user = User.getUser(userId); if (user != null) { Order order = user.getOrder(); if (order != null) { Address address = order.getAddress(); if (address != null) { String detail = address.getDetail(); if (detail != null) { return detail; } } } } return "不好意思,找了半天,沒找到"; }
還好,Java 8 中引入的 Optional 類可以簡化這個流程。
public static String getUserOrderDetail(Integer userId) { return Optional.ofNullable(User.getUser(userId)) .map(User::getOrder) .map(Order::getAddress) .map(Address::getDetail) .orElse("不好意思,找了半天,沒找到"); }
2、 能不返回 NULL 的話,就儘量不返回 NULL
比如有些獲取集合的方法,沒有結果的話,可以返回一個空列表。這種方式對於提供給前端或者消費者使用的介面更加適用,返回一個空集合要遠比返回一個空更友好。
3、 能拋異常的話,寧可拋異常,也不要返回 NULL
還有一些情況,丟擲給呼叫者一個具體的異常,要比返回一個 NULL 更加能讓呼叫者清楚到底發生了什麼。
比如根據一個使用者的資訊,但是發現使用者不存在了,直接返回給呼叫者一個「使用者不存在」的異常資訊更明確,而不是返回一個 NULL,讓呼叫方去猜。