厭倦了空指針異常? 考慮使用Java SE 8的Optional分扎!使代碼更具可讀性并使得免受空指針異常的影響馍资。
有人曾經(jīng)說(shuō)過(guò)悲没,在未處理空指針異常之前,你不是真正的Java程序員聊替。 開(kāi)玩笑說(shuō),空引用是許多問(wèn)題的根源,因?yàn)樗ǔ1硎救鄙僦怠?Java SE 8引入了一個(gè)名為java.util.Optional的新類眶拉,可以緩解一些這樣的問(wèn)題。
讓我們從一個(gè)例子開(kāi)始憔儿,看看空指針的危險(xiǎn)性忆植。 下面是一個(gè)計(jì)算機(jī)的嵌套對(duì)象結(jié)構(gòu),如圖所示:
下面的代碼可能會(huì)產(chǎn)生什么問(wèn)題谒臼?
String version = computer.getSoundcard().getUSB().getVersion();
這段代碼看起來(lái)沒(méi)什么問(wèn)題呀朝刊。但是,好多計(jì)算機(jī)(比如Raspberry Pi)實(shí)際上并沒(méi)有安裝聲卡(sound card)蜈缤,那么getSoundcard()得到得結(jié)果是什么呢拾氓?
那么一般來(lái)說(shuō)是返回空引用表示沒(méi)有聲卡。 不幸的是劫樟,getUSB()將嘗試返回空引用的USB端口痪枫,這將在運(yùn)行時(shí)導(dǎo)致NullPointerException织堂,程序奔潰。 想象一下奶陈,如果你的程序在生產(chǎn)環(huán)境上運(yùn)行; 如果程序突然報(bào)錯(cuò)易阳,你的客戶會(huì)有什么反應(yīng)?
空指針是有一些歷史背景吃粒,計(jì)算機(jī)科學(xué)巨頭Tony Hoare寫道: "I call it my billion-dollar mistake. It was the invention of the null reference in 1965. I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement."
怎么做才能防止空指針異常發(fā)生呢潦俺?你可以通過(guò)判斷是否為null來(lái)防止NullPointerException,如下所示:
例1
String version = "UNKNOWN";
if(computer != null){
Soundcard soundcard = computer.getSoundcard();
if(soundcard != null){
USB usb = soundcard.getUSB();
if(usb != null){
version = usb.getVersion();
}
}
}
但是徐勃,通過(guò)例1我們看到事示,代碼由于嵌套檢查變得非常難看。不幸的是僻肖,我們需要很多這樣得代碼來(lái)確保不會(huì)發(fā)生NullPointerException肖爵。 此外,業(yè)務(wù)邏輯中若含有這些檢查臀脏,會(huì)讓我們很是厭煩劝堪。 事實(shí)上,這些代碼也降低我們系統(tǒng)的整體可讀性揉稚。
此外秒啦,嵌套檢查也是一個(gè)容易出錯(cuò)的過(guò)程; 如果你忘記檢查一個(gè)屬性是否為空怎么辦? 本文將論證使用null表示空值是一種錯(cuò)誤的方法搀玖。 我們需要一種更好的方法來(lái)表達(dá)空值和非空值余境。
為了給出一些上下文,讓我們簡(jiǎn)要介紹一下其他編程語(yǔ)言提供的解決方式灌诅。
其他語(yǔ)言的替代方案是什么芳来?
例如Groovy、C#等語(yǔ)言有一個(gè)由"?."表示的安全導(dǎo)航操作符猜拾,用來(lái)保護(hù)出現(xiàn)在屬性路徑中 null 值绣张。如下所示:
String version = computer?.getSoundcard()?.getUSB()?.getVersion();
在這種情況下,如果computer為null关带,則變量version 將被賦值為null,或者getSoundcard() 返回null沼撕,或者getUSB()返回null宋雏。 你不需要編寫復(fù)雜的嵌套條件來(lái)檢查是否為null。
此外务豺,Groovy還包括Elvis運(yùn)算符"?:"(至于為什么叫Elvis運(yùn)算符磨总,是因?yàn)??:'跟一個(gè)叫Elvis的搖滾明星(貓王)的發(fā)型很像。)笼沥,當(dāng)需要默認(rèn)值時(shí)蚪燕,使用Elvis運(yùn)算符會(huì)使表達(dá)式更簡(jiǎn)潔娶牌。 下面,如果使用安全導(dǎo)航操作符的表達(dá)式返回null馆纳,則返回默認(rèn)值"UNKNOWN"; 否則诗良,返回可用的version。如下所示:
String version =
computer?.getSoundcard()?.getUSB()?.getVersion() ?: "UNKNOWN";
其他函數(shù)語(yǔ)言(如Haskell和Scala)采用不同的解決方案鲁驶。 Haskell包含一個(gè)Maybe類型鉴裹,它基本上封裝了一個(gè)可選值。 Maybe類型的值可以包含給定類型的值钥弯,也可以不包含任何值径荔,沒(méi)有空引用的概念。 Scala有一個(gè)名為Option[T]的類似構(gòu)造來(lái)封裝類型T值的存在或缺失脆霎。然后总处,你必須使用Option顯式檢查是否存在值,這強(qiáng)制了"null checking.". 你再也不能“忘記這樣做”睛蛛,因?yàn)樗怯深愋拖到y(tǒng)強(qiáng)制執(zhí)行的鹦马。
好吧,我們似乎偏離了主題玖院,而且這些聽(tīng)起來(lái)都相當(dāng)抽象菠红。 你現(xiàn)在可能會(huì)想,“那么难菌,Java SE 8呢试溯?”
Optional 介紹
Java SE 8引入了一個(gè)名為 java.util.Optional<T>的新類,受Haskell和Scala思想的啟發(fā)郊酒。 它是一個(gè)封裝可選值的類遇绞,你可以將Optional視為一個(gè)單值容器,可以包含值燎窘,也不包含值的(然后將其視為“空” )摹闽。如圖所示:
接下來(lái),我們可以使用Optional更改一下例1的代碼:
例2
public class Computer {
private Optional<Soundcard> soundcard;
public Optional<Soundcard> getSoundcard() { ... }
...
}
public class Soundcard {
private Optional<USB> usb;
public Optional<USB> getUSB() { ... }
}
public class USB{
public String getVersion(){ ... }
}
例2中的代碼立即顯示計(jì)算機(jī)可能有聲卡,也可能沒(méi)有聲卡(聲卡是可選的)褐健。 此外付鹿,聲卡可以選配USB端口。 這是一種改進(jìn)蚜迅,因?yàn)檫@個(gè)新模型現(xiàn)在可以清楚地反映出是否允許丟失給定值舵匾。 請(qǐng)注意,類似的想法已在諸如Guava等類庫(kù)中早已提供谁不。
但是我們以用Optional<Soundcard>對(duì)象實(shí)際做些什么呢坐梯? 最終只是想要獲得USB端口的版本號(hào)。 簡(jiǎn)而言之刹帕,Optional類包括處理存在或不存在值的情況的方法吵血。 但是谎替,與空引用(null)相比的優(yōu)點(diǎn)是:Optional類強(qiáng)制你在值不存在時(shí)考慮該情況。 因此蹋辅,你能更有效地防止代碼中出現(xiàn)不期而至的空指針異常钱贯。
值得注意的是,Optional類的意圖不是替換空引用晕翠。 相反喷舀,它的目的是幫助設(shè)計(jì)更易于理解的API,這樣只需讀取方法的簽名淋肾,就能了解該方法是否接受一個(gè)Optional類型的值硫麻。 這會(huì)強(qiáng)制你主動(dòng)解包Optional以處理空值。
采用Optional模式
廢話我們就不多說(shuō)了; 讓我們看看代碼吧樊卓! 首先我們將探討如何使用Optional重寫典型的空檢查模式拿愧。 在本文結(jié)束時(shí),你將了解如何使用Optional來(lái)重寫例1中執(zhí)行多個(gè)嵌套空檢查碌尔,如下所示:
String name = computer.flatMap(Computer::getSoundcard)
.flatMap(Soundcard::getUSB)
.map(USB::getVersion)
.orElse("UNKNOWN");
Note: 確保了解Java SE 8 lambdas和方法引用語(yǔ)法(請(qǐng)參閱Java 8:Lambdas)及其流管道概念(請(qǐng)參閱使用Java SE 8 Streams處理數(shù)據(jù))浇辜。
創(chuàng)建Optional對(duì)象
首先,如何創(chuàng)建Optional對(duì)象呢唾戚? 有如下幾種方法:
- 聲明一個(gè)空的Optional
Optional<Soundcard> sc = Optional.empty();
- 依據(jù)一個(gè)非空值創(chuàng)建Optional
SoundCard soundcard = new Soundcard();
Optional<Soundcard> sc = Optional.of(soundcard);
如果soundcard是一個(gè)null柳洋,這段代碼會(huì)立即拋出一個(gè)NullPointerException,而不是等到你試圖訪問(wèn)soundcard 的屬性值時(shí)才返回一個(gè)錯(cuò)誤叹坦。
- 可接受null的Optional
最后熊镣,使用靜態(tài)工廠方法Optional.ofNullable,你可以創(chuàng)建一個(gè)允許null值的Optional 對(duì)象:
Optional<Soundcard> sc = Optional.ofNullable(soundcard);
如果Soundcard是null募书,那么得到的Optional對(duì)象就是個(gè)空對(duì)象绪囱。
如果值存在,那么就做點(diǎn)什么吧
現(xiàn)在你有了一個(gè)Optional對(duì)象莹捡,你可以使用可用的方法來(lái)顯式處理其值鬼吵,無(wú)論值存在與否。 而不是必須進(jìn)行空檢查篮赢,如下所示:
SoundCard soundcard = ...;
if(soundcard != null){
System.out.println(soundcard);
}
上訴代碼你可以使用* ifPresent()*進(jìn)行重寫齿椅,如下所示:
Optional<Soundcard> soundcard = ...;
soundcard.ifPresent(System.out::println);
你無(wú)需再進(jìn)行顯式空檢查; 它由類型系統(tǒng)強(qiáng)制執(zhí)行。 如果Optional對(duì)象為空启泣,則不會(huì)打印任何內(nèi)容媒咳。
你可以使用isPresent()方法來(lái)判斷Optional對(duì)象中是否存在值。此外种远,還由一個(gè)get()方法,顽耳。如果變量存在坠敷,它直接返回封裝的變量 值妙同,否則就拋出一個(gè)NoSuchElementException異常。這兩個(gè)方法組合使用膝迎,可以防止異常的發(fā)生粥帚。如下所示:
if(soundcard.isPresent()){
System.out.println(soundcard.get());
}
但是,這不是Optional的推薦用法(它對(duì)嵌套空值檢查沒(méi)有太大改進(jìn))限次,還有更好的替代方案芒涡,我們將在下面討論。
默認(rèn)行為及解引用 Optional 對(duì)象
如果返回值為null卖漫,通常的處理方式是給定一個(gè)默認(rèn)值费尽。 一般來(lái)說(shuō),你可以使用三元運(yùn)算符來(lái)實(shí)現(xiàn)此目的羊始,如下所示:
Soundcard soundcard =
maybeSoundcard != null ? maybeSoundcard
: new Soundcard("basic_sound_card");
使用Optional對(duì)象, 你可以使用orElse()方法重寫上面的代碼 旱幼,使用這種方式你還可以定義一個(gè)默認(rèn)值,遭遇空的Optional變量時(shí)突委,默認(rèn)值會(huì)作為該方法的調(diào)用返回值柏卤。如下所示:
Soundcard soundcard = maybeSoundcard.orElse(new Soundcard("defaut"));
同理,你也可以使用orElseThrow()方法匀油,與orElse()方法不同的是缘缚,使用orElseThrow()時(shí),當(dāng)遭遇Optional對(duì)象為空時(shí)都會(huì)拋出一個(gè)異常敌蚜,但是使用orElseThrow你可以定制希 望拋出的異常類型桥滨。 如下所示:
Soundcard soundcard =
maybeSoundCard.orElseThrow(IllegalStateException::new);
使用filter 剔除特定的值
你經(jīng)常需要調(diào)用某個(gè)對(duì)象的方法,查看它的某些屬性钝侠。比如该园,你可能需要檢查USB端口是否為3.0版本。為了以一種安全的方式進(jìn)行這些操作帅韧,你首先需要確定引用指向的USB 對(duì)象是否為null里初,之后再調(diào)用它的getVersion()方法,如下所示:
USB usb = ...;
if(usb != null && "3.0".equals(usb.getVersion())){
System.out.println("ok");
}
使用Optional對(duì)象的filter方法忽舟,這段代碼可以重構(gòu)如下:
Optional<Insurance> optInsurance = ...; optInsurance.filter(insurance -> "CambridgeInsurance".equals(insurance.getName())) .ifPresent(x -> System.out.println("ok"));
filter方法接受一個(gè)謂詞作為參數(shù)双妨。如果Optional對(duì)象的值存在,并且它符合謂詞的條件叮阅, filter方法就返回其值刁品;否則它就返回一個(gè)空的Optional對(duì)象。你可以將 Optional看成包含一個(gè)元素的Stream對(duì)象浩姥,這個(gè)方法的行為就非常清晰了挑随。如果Optional 對(duì)象為空,它不做任何操作勒叠,反之兜挨,它就對(duì)Optional對(duì)象中包含的值施加謂詞操作膏孟。如果該操 作的結(jié)果為true,它不做任何改變拌汇,直接返回該Optional對(duì)象柒桑,否則就將該值過(guò)濾掉,將 Optional的值置空噪舀。
使用map 從 Optional 對(duì)象中提取和轉(zhuǎn)換值
從對(duì)象中提取信息是一種比較常見(jiàn)的模式魁淳。比如,你可能想要從Soundcard 對(duì)象中提取USB對(duì)象与倡。提取之前界逛,你需要檢查Soundcard對(duì)象是否為null,然后進(jìn)一步檢查它的version是否正確蒸走,你可能會(huì)寫如下代碼:
if(soundcard != null){
USB usb = soundcard.getUSB();
if(usb != null && "3.0".equals(usb.getVersion()){
System.out.println("ok");
}
}
我們可以使用map方法重寫這種 "checking for null and extracting" (這里是Soundcard對(duì)象)的模式仇奶。
Optional<Soundcard> maybeSoundcard= Optional.ofNullable(soundcard); Optional<USB> usb = maybeSoundcard.map(Soundcard::getUSB);
從概念上,這與Stream的map方法相差無(wú)幾比驻。map操作會(huì)將提供的函數(shù)應(yīng)用于流的每個(gè)元素该溯。如 果Stream為空,就什么也不做别惦。
Optional類的map方法完全相同:你可以把Optional對(duì)象看成一種特殊的集合數(shù)據(jù)狈茉,它至多包含一個(gè)元素。如果Optional包含一個(gè)值掸掸,那函數(shù)(這里是提取USB端口的方法引用)就將該值作為參數(shù)傳遞給map氯庆,對(duì)該值進(jìn)行轉(zhuǎn)換。如果Optional為空扰付,就什么也不做堤撵。
最后,我們可以結(jié)合map方法和filter方法重寫上面的代碼羽莺,剔除版本不同于3.0的USB端口:
maybeSoundcard.map(Soundcard::getUSB)
.filter(usb -> "3.0".equals(usb.getVersion())
.ifPresent(() -> System.out.println("ok"));
真棒; 我們的代碼開(kāi)始更接近問(wèn)題陳述实昨,并且沒(méi)有重復(fù)的嵌套空檢查妨礙我們!
使用flatMap 鏈接 Optional 對(duì)象
我們已經(jīng)使用Optional重構(gòu)了一些以前的代碼盐固,那么我們?nèi)绾我园踩姆绞骄帉懸韵麓a呢荒给?
String version = computer.getSoundcard().getUSB().getVersion();
請(qǐng)注意,這些代碼的意思都是從另一個(gè)對(duì)象中提取一個(gè)對(duì)象刁卜,這正是map方法的用處志电。 在本文前面,我們更改了model蛔趴,因此Computer具有Optional<Soundcard>挑辆,Soundcard 具有 Optional<USB>,因此我們可以利用map重寫之前的代碼:
String version = computer.map(Computer::getSoundcard)
.map(Soundcard::getUSB)
.map(USB::getVersion)
.orElse("UNKNOWN");
不幸的是,這段代碼無(wú)法通過(guò)編譯之拨。為什么呢茉继?computer是 Optional<Computer>類型的變量, 調(diào)用map方法應(yīng)該沒(méi)有問(wèn)題蚀乔。但getSoundcard() 返回的是一個(gè)Optional<Soundcard>類型的對(duì)象,這意味著map操作的結(jié)果是一個(gè)Optional<Optional<Soundcard>>類型的對(duì)象菲茬。因 此吉挣,它對(duì)getUSB()的調(diào)用是非法的,因?yàn)樽钔鈱拥膐ptional對(duì)象包含了另一個(gè)optional 對(duì)象的值婉弹,而它當(dāng)然不會(huì)支持e getUSB()方法睬魂。下圖說(shuō)明了你會(huì)遭遇的嵌套式optional 結(jié)構(gòu)。
所以镀赌,我們?cè)撊绾谓鉀Q這個(gè)問(wèn)題呢氯哮?讓我們?cè)倩仡櫼幌略诹魃鲜褂眠^(guò)的模式: flatMap方法。使用流時(shí)商佛,flatMap方法接受一個(gè)函數(shù)作為參數(shù)喉钢,這個(gè)函數(shù)的返回值是另一個(gè)流。 這個(gè)方法會(huì)應(yīng)用到流中的每一個(gè)元素良姆,終形成一個(gè)新的流的流肠虽。但是flagMap會(huì)用流的內(nèi)容替 換每個(gè)新生成的流。換句話說(shuō)玛追,由方法生成的各個(gè)流會(huì)被合并或者扁平化為一個(gè)單一的流税课。這里你希望的結(jié)果其實(shí)也是類似的,但是你想要的是將兩層的optional合并為一個(gè)痊剖。
好吧韩玩,這是個(gè)好消息:Optional也支持flatMap方法。 它的目的是將轉(zhuǎn)換函數(shù)應(yīng)用于Optional的值(就像map操作一樣)陆馁,然后將兩層的optional合并為一個(gè)找颓。 下圖說(shuō)明了transform函數(shù)返回Optional對(duì)象時(shí)map和flatMap之間的區(qū)別。
因此氮惯,相信現(xiàn)在你已經(jīng)對(duì)Optional的map和flatMap方法有了一定的了解叮雳,讓我們看看如何應(yīng)用。我們需要使用flatMap重寫上面的代碼妇汗,如下所示:
String version = computer.flatMap(Computer::getSoundcard)
.flatMap(Soundcard::getUSB)
.map(USB::getVersion)
.orElse("UNKNOWN");
第一個(gè)flatMap確保返回Optional<Soundcard> 對(duì)象帘不,而不是Optional<Optional<Soundcard>>,同理杨箭,第二個(gè)flatMap返回 Optional<USB>對(duì)象寞焙。 請(qǐng)注意,第三個(gè)調(diào)用需要map()方法,因?yàn)間etVersion()返回String對(duì)象捣郊,而不是Optional對(duì)象辽狈。
哇! 從編寫痛苦的嵌套空檢查到編寫組合代碼呛牲,再到可讀性強(qiáng)刮萌,更有效地防止代碼中出現(xiàn)不期而至的空指針異常,我們已經(jīng)做的越來(lái)越好了娘扩。
結(jié)論
在本文中着茸,我們已經(jīng)學(xué)習(xí)了如何采用新的Java SE 8 java.util.Optional<T>。 Optional的目的不是替換代碼中的每個(gè)空引用琐旁,而是幫助設(shè)計(jì)更好的API涮阔,只需讀取方法的簽名 - 就能了解該方法是否接受一個(gè)Optional類型的值。 此外灰殴,Optional強(qiáng)制主動(dòng)解包Optional以處理空值; 因此敬特,可以保護(hù)代碼免受意外的空指針異常的影響。
相關(guān)代碼請(qǐng)參見(jiàn)我的github optionalExample
附錄:
方 法 | 描 述 |
---|---|
empty | 返回一個(gè)空的 Optional 實(shí)例 |
filter | 如果值存在并且滿足提供的謂詞牺陶,就返回包含該值的 Optional 對(duì)象伟阔;否則返回一個(gè)空的 Optional 對(duì)象 |
flatMap | 如果值存在,就對(duì)該值執(zhí)行提供的 mapping函數(shù)調(diào)用义图,返回一個(gè) Optional 類型的值减俏,否則就返 回一個(gè)空的 Optional 對(duì)象 |
get | 如果該值存在,將該值用 Optional 封裝返回碱工,否則拋出一個(gè) NoSuchElementException 異常 |
ifPresent | 如果值存在娃承,就執(zhí)行使用該值的方法調(diào)用,否則什么也不做 |
isPresent | 如果值存在就返回 true怕篷,否則返回 false |
map | 如果值存在历筝,就對(duì)該值執(zhí)行提供的 mapping函數(shù)調(diào)用 |
of | 將指定值用 Optional 封裝之后返回,如果該值為 null廊谓,則拋出一個(gè) NullPointerException 異常 |
ofNullable | 將指定值用 Optional 封裝之后返回梳猪,如果該值為 null,則返回一個(gè)空的 Optional 對(duì)象 |
orElse | 如果有值則將其返回蒸痹,否則返回一個(gè)默認(rèn)值 |
orElseGet | 如果有值則將其返回春弥,否則返回一個(gè)由指定的 Supplier 接口生成的值 |
orElseThrow | 如果有值則將其返回,否則拋出一個(gè)由指定的 Supplier 接口生成的異常 |