Java 14 發布了,再也不怕 NullPointerException 了!

  • 2020 年 3 月 26 日
  • 筆記

2020年3月17日發布,Java正式發布了JDK 14 ,目前已經可以開放下載。在JDK 14中,共有16個新特性,本文主要來介紹其中的一個特性:JEP 358: Helpful NullPointerExceptions

null何錯之有?

對於Java程式設計師來說,null是令人頭痛的東西。時常會受到空指針異常(NullPointerException)的騷擾。相信很多程式設計師都特別害怕出現程式中出現NPE,因為這種異常往往伴隨著程式碼的非預期運行。

在程式語言中,空引用(Null Reference)是一個與空指針類似的概念,是一個已宣告但其並未引用到一個有效對象的變數。

在Java 1 中就包含了了Null引用和NPE了,但是其實,Null引用是偉大的電腦科學家Tony Hoare 早在1965年發明的,最初作為程式語言ALGOL W的一部分。

1965年,英國一位名為Tony Hoare的電腦科學家在設計ALGOL W語言時提出了null引用的想法。ALGOL W是第一批在堆上分配記錄的類型語言之一。Hoare選擇null引用這種方式,「只是因為這種方法實現起來非常容易」。雖然他的設計初衷就是要「通過編譯器的自動檢測機制,確保所有使用引用的地方都是絕對安全的」,他還是決定為null引用開個綠燈,因為他認為這是為「不存在的值」建模最容易的方式。

但是在2009年,很多年後,他開始為自己曾經做過這樣的決定而後悔不已,把它稱為「一個價值十億美元的錯誤」。實際上,Hoare的這段話低估了過去五十年來數百萬程式設計師為修復空引用所耗費的代價。因為在ALGOL W之後出現的大多數現代程式設計語言,包括Java,都採用了同樣的設計方式,其原因是為了與更老的語言保持兼容,或者就像Hoare曾經陳述的那樣,「僅僅是因為這樣實現起來更加容易」。

相信很多Java程式設計師都一樣對null和NPE深惡痛絕,因為他確實會帶來各種各樣的問題(來自《Java 8 實戰》)。如:

  • 它是錯誤之源。 NullPointerException是目前Java程式開發中最典型的異常。它會使你的程式碼膨脹。
  • 它讓你的程式碼充斥著深度嵌套的null檢查,程式碼的可讀性糟糕透頂。
  • 它自身是毫無意義的。 null自身沒有任何的語義,尤其是是它代表的是在靜態類型語言中以一種錯誤的方式對缺失變數值的建模。
  • 它破壞了Java的哲學。 Java一直試圖避免讓程式設計師意識到指針的存在,唯一的例外是:null指針。
  • 它在Java的類型系統上開了個口子。 null並不屬於任何類型,這意味著它可以被賦值給任意引用類型的變數。這會導致問題, 原因是當這個變數被傳遞到系統中的另一個部分後,你將無法獲知這個null變數最初賦值到底是什麼類型。

其他語言如何解決NPE問題

我們知道,出了Java語言外,還有很多其他的面向對象語言,那麼在其他的一些語言中,是如何解決NPE的問題的呢?

如在Groovy中使用安全導航操作符(Safe Navigation Operator)可以訪問可能為null的變數:

def carInsuranceName = person?.car?.insurance?.name  

Groovy的安全導航操作符能夠避免在訪問這些可能為null引用的變數時發生NullPointerException,在調用鏈中的變數遭遇null時將null引用沿著調用鏈傳遞下去,返回一個null。

其實這個功能曾經考慮過增加一個類似的功能,但是後來又被捨棄了。

另外,在Haskell和Scala也有類似的替代品,如Haskell中的Maybe類型、Scala中的Option[T]。

在 Kotlin 中,其類型系統嚴格區分一個引用可以容納 null 還是不能容納。也就是說,一個變數是否可空必須顯示聲明,對於可空變數,在訪問其成員時必須做空處理,否則無法編譯通過:

var a: String = "abc"  a = null // 編譯錯誤  

果允許為空,可以聲明一個可空字元串,寫作 String?:

var b: String? = "abc" //String? 表示該 String 類型變數可為空  b = null // 編譯通過  

看到這個?的時候,是不是發現和Groovy有點像?不過還是有一定區別的,這裡就不展開了。

好了,書歸正傳,我們來看看作為一個TOIBE程式語言排行榜第一名的語言,Java語言對於NPE做出了哪些努力!

Java做了哪些努力

一直以來對於null和NPE的改進還是做出了一些努力的。

首先在Java 8中提供了Optional,其實在Java 8 推出之前,Google的Guava庫中就率先提供過Optional介面來使null快速失敗。

Optional在可能為null的對象上做了一層封裝,Optional對象包含了一些方法來顯式地處理某個值是存在還是缺失,Optional類強制你思考值不存在的情況,這樣就能避免潛在的空指針異常。

但是設計Optional類的目的並不是完全取代null,它的目的是設計更易理解的API。通過Optional,可以從方法簽名就知道這個函數有可能返回一個缺失的值,這樣強制你處理這些缺失值的情況。

關於Optional的用法,不是本文的重點,就不在這裡詳細介紹了,筆者在日常開發中經常結合Stream一起使用Optional,還是比較好用的。

另外一個值得一提的就是最近(2020年03月17日)發布的JDK 14中對於NPE有了一個增強。那就是JEP 358: Helpful NullPointerExceptions

更有幫助的NPE

JDK 14中對於NEP有了一個增強,既然NPE暫時無法避免,那麼就讓他對開發者更有幫助一些。

-w602

每個Java開發人員都遇到過NullPointerExceptions (NPEs)。由於NPEs可以發生在程式的幾乎任何地方,試圖捕獲並從它們中恢復通常是不切實際的。因此,開發人員通常依賴於JVM來確定NPE實際發生時的來源。例如,假設在這段程式碼中出現了一個NPE:

a.i = 99;  

JVM將列印出導致NPE的方法、文件名和行號:

Exception in thread "main" java.lang.NullPointerException  at Prog.main(Prog.java:5)  

通過以上堆棧資訊,開發人員可以定位到a.i= 99這一行,並推斷出a一定是null。

但是,對於更複雜的程式碼,如果不使用調試器,就不可能確定哪個變數是null。假設在這段程式碼中出現了一個NPE:

a.b.c.i = 99;  

我們根本無法確定到底是a還是b或者是c在運行時是個null值。

但是,在JDK14以後,這種窘境就有解了。

在JDK14中,當運行期,試圖對一個bull對象進行應用時,JVM依然會拋出一個NullPointerException (NPE),除此之外,還會通過通過分析程式的位元組碼指令,JVM將精確地確定哪個變數是null,並且在堆棧資訊中明確的提示出來。

在JDK 14中,如果上文中的a.i = 99發生NPE,將會列印如下堆棧:

Exception in thread "main" java.lang.NullPointerException:          Cannot assign field "i" because "a" is null      at Prog.main(Prog.java:5)  

如果是a.b.c.i = 99;中的b為null導致了空指針,則會列印以下堆棧資訊:

Exception in thread "main" java.lang.NullPointerException:          Cannot read field "c" because "a.b" is null      at Prog.main(Prog.java:5)  

可見,堆棧中明確指出了到底是哪個對象為null而導致了NPE,這樣,一旦應用中發生NPE,開發者可以通過堆棧資訊第一時間定位到到底是程式碼中的那個對象為null導致的。

這算是JDK的一個小小的改進,但是這個改進對於開發者來說確實是非常友好的。真的希望這些小而美的改動可以在JDK中越來越多。

參考資料:

https://openjdk.java.net/jeps/358

《Java 8 In Action》