如何預(yù)防 Java 中著名的 NullPointerException 異常?這是每個(gè) Java 初學(xué)者遲早會(huì)問到的關(guān)鍵問題之一。而且中級(jí)和高級(jí)程序員也在時(shí)時(shí)刻刻規(guī)避這個(gè)錯(cuò)誤。其是迄今為止 Java 以及很多其他編程語言中最流行的一種錯(cuò)誤。
Null 引用的發(fā)明者 Tony Hoare 在 2009 年道歉,并稱這種錯(cuò)誤為他的十億美元錯(cuò)誤。
我將其稱之為自己的十億美元錯(cuò)誤。它的發(fā)明是在1965 年,那時(shí)我用一個(gè)面向?qū)ο笳Z言(ALGOL W)設(shè)計(jì)了第一個(gè)全面的引用類型系統(tǒng)。我的目的是確保所有引用的使用都是絕對(duì)安全的,編譯器會(huì)自動(dòng)進(jìn)行檢查。但是我未能抵御住誘惑,加入了 Null 引用,僅僅是因?yàn)閷?shí)現(xiàn)起來非常容易。它導(dǎo)致了數(shù)不清的錯(cuò)誤、漏洞和系統(tǒng)崩潰,可能在之后 40 年中造成了十億美元的損失。
無論如何,我們必須要面對(duì)它。所以,我們到底能做些什么來防止 NullPointerException 異常呢?那么,答案顯然是對(duì)其添加 null 檢查。由于 null 檢查還是挺麻煩和痛苦的,很多語言為了處理 null 檢查添加了特殊的語法,即空合并運(yùn)算符 —— 其在像 Groovy 或 Kotlin 這樣的語言中也被稱為 Elvis 運(yùn)算符。
不幸的是 Java 沒有提供這樣的語法糖。但幸運(yùn)的是這在 Java 8 中得到了改善。這篇文章介紹了如何利用像 lambda 表達(dá)式這樣的 Java 8 新特性來防止編寫不必要的 null 檢查的幾個(gè)技巧。
在 Java 8 中提高 Null 的安全性
在另一篇文章中說明了我們可以如何利用 Java 8 的 Optional 類型來預(yù)防 null 檢查。下面是那篇文章中的示例代碼。
假設(shè)我們有一個(gè)像這樣的類層次結(jié)構(gòu):
class Outer {
Nested nested;
Nested getNested() {
return nested;
}
}
class Nested {
Inner inner;
Inner getInner() {
return inner;
}
}
class Inner {
String foo;
String getFoo() {
return foo;
}
}
解決這種結(jié)構(gòu)的深層嵌套路徑是有點(diǎn)麻煩的。我們必須編寫一堆 null 檢查來確保不會(huì)導(dǎo)致一個(gè) NullPointerException:
Outer outer = new Outer();
if (outer != null && outer.nested != null && outer.nested.inner != null) {
System.out.println(outer.nested.inner.foo);
}
我們可以通過利用 Java 8 的 Optional 類型來擺脫所有這些 null 檢查。map 方法接收一個(gè) Function 類型的 lambda 表達(dá)式,并自動(dòng)將每個(gè) function 的結(jié)果包裝成一個(gè) Optional 對(duì)象。這使我們能夠在一行中進(jìn)行多個(gè) map 操作。Null 檢查是在底層自動(dòng)處理的。
Optional.of(new Outer())
.map(Outer::getNested)
.map(Nested::getInner)
.map(Inner::getFoo)
.ifPresent(System.out::println);
還有一種實(shí)現(xiàn)相同作用的方式就是通過利用一個(gè) supplier 函數(shù)來解決嵌套路徑的問題:
Outer obj = new Outer();
resolve(() -> obj.getNested().getInner().getFoo());
.ifPresent(System.out::println);
調(diào)用 obj.getNested().getInner().getFoo()) 可能會(huì)拋出一個(gè) NullPointerException 異常。在這種情況下,該異常將會(huì)被捕獲,而該方法會(huì)返回 Optional.empty()。
public static <T> Optional<T> resolve(Supplier<T> resolver) {
try {
T result = resolver.get();
return Optional.ofNullable(result);
}
catch (NullPointerException e) {
return Optional.empty();
}
}
請(qǐng)記住,這兩個(gè)解決方案可能沒有傳統(tǒng) null 檢查那么高的性能。不過在大多數(shù)情況下不會(huì)有太大問題。
像往常一樣,上面的示例代碼都托管在 GitHub。
祝編程愉快!