Java函數(shù)式開(kāi)發(fā) Optional空指針處理
摘要
空閑時(shí)會(huì)抽空學(xué)習(xí)同在jvm上運(yùn)行的Groovy和Scala,發(fā)現(xiàn)他們對(duì)null的處理比早期版本Java慎重很多。在Java8中,Optional為函數(shù)式編程的null處理給出了非常優(yōu)雅的解決方案。本文將說(shuō)明長(zhǎng)久以來(lái)Java中對(duì)null的蹩腳處理,然后介紹使用Optional來(lái)實(shí)現(xiàn)Java函數(shù)式編程。
那些年困擾著我們的null
在Java江湖流傳著這樣一個(gè)傳說(shuō):直到真正了解了空指針異常,才能算一名合格的Java開(kāi)發(fā)人員。在我們逼格閃閃的java碼字符生涯中,每天都會(huì)遇到各種null的處理,像下面這樣的代碼可能我們每天都在反復(fù)編寫:
if(null != obj1){ if(null != obje2){ // do something } }
稍微有點(diǎn)眼界javaer就去干一些稍有逼格的事,弄一個(gè)判斷null的方法:
boolean checkNotNull(Object obj){ return null == obj ? false : true; } void do(){ if(checkNotNull(obj1)){ if(checkNotNull(obj2)){ //do something } } }
然后,問(wèn)題又來(lái)了:如果一個(gè)null表示一個(gè)空字符串,那”"表示什么?
然后慣性思維告訴我們,”"和null不都是空字符串碼?索性就把判斷空值升級(jí)了一下:
boolean checkNotBlank(Object obj){ return null != obj && !"".equals(obj) ? true : false; } void do(){ if(checkNotBlank(obj1)){ if(checkNotNull(obj2)){ //do something } } }
有空的話各位可以看看目前項(xiàng)目中或者自己過(guò)往的代碼,到底寫了多少和上面類似的代碼。
不知道你是否認(rèn)真思考過(guò)一個(gè)問(wèn)題:一個(gè)null到底意味著什么?
- 淺顯的認(rèn)識(shí)——null當(dāng)然表示“值不存在”。
- 對(duì)內(nèi)存管理有點(diǎn)經(jīng)驗(yàn)的理解——null表示內(nèi)存沒(méi)有被分配,指針指向了一個(gè)空地址。
- 稍微透徹點(diǎn)的認(rèn)識(shí)——null可能表示某個(gè)地方處理有問(wèn)題了,也可能表示某個(gè)值不存在。
- 被虐千萬(wàn)次的認(rèn)識(shí)——哎喲,又一個(gè)NullPointerException異常,看來(lái)我得加一個(gè)if(null != value)了。
回憶一下,在咱們前面碼字生涯中到底遇到過(guò)多少次java.lang.NullPointerException異常?NullPointerException作為一個(gè)RuntimeException級(jí)別的異常不用顯示捕獲,若不小心處理我們經(jīng)常會(huì)在生產(chǎn)日志中看到各種由NullPointerException引起的異常堆棧輸出。而且根據(jù)這個(gè)異常堆棧信息我們根本無(wú)法定位到導(dǎo)致問(wèn)題的原因,因?yàn)椴⒉皇菕伋鯪ullPointerException的地方引發(fā)了這個(gè)問(wèn)題。我們得更深處去查詢什么地方產(chǎn)生了這個(gè)null,而這個(gè)時(shí)候日志往往無(wú)法跟蹤。
有時(shí)更悲劇的是,產(chǎn)生null值的地方往往不在我們自己的項(xiàng)目代碼中。這就存在一個(gè)更尷尬的事實(shí)——在我們調(diào)用各種良莠不齊第三方接口時(shí),說(shuō)不清某個(gè)接口在某種機(jī)緣巧合的情況下就會(huì)返回一個(gè)null……
回到前面對(duì)null的認(rèn)知問(wèn)題。很多javaer認(rèn)為null就是表示“什么都沒(méi)有”或者“值不存在”。按照這個(gè)慣性思維我們的代碼邏輯就是:你調(diào)用我的接口,按照你給我的參數(shù)返回對(duì)應(yīng)的“值”,如果這條件沒(méi)法找到對(duì)應(yīng)的“值”,那我當(dāng)然返回一個(gè)null給你表示沒(méi)有“任何東西”了。我們看看下面這個(gè)代碼,用很傳統(tǒng)很標(biāo)準(zhǔn)的Java編碼風(fēng)格編寫:
class MyEntity{ int id; String name; String getName(){ return name; } } // main public class Test{ public static void main(String[] args) final MyEntity myEntity = getMyEntity(false); System.out.println(myEntity.getName()); } private getMyEntity(boolean isSuc){ if(isSuc){ return new MyEntity(); }else{ return null; } } }
這一段代碼很簡(jiǎn)單,日常的業(yè)務(wù)代碼肯定比這個(gè)復(fù)雜的多,但是實(shí)際上我們大量的Java編碼都是按這種套路編寫的,懂貨的人一眼就可以看出最終肯定會(huì)拋出NullPointerException。但是在我們編寫業(yè)務(wù)代碼時(shí),很少會(huì)想到要處理這個(gè)可能會(huì)出現(xiàn)的null(也許API文檔已經(jīng)寫得很清楚在某些情況下會(huì)返回null,但是你確保你會(huì)認(rèn)真看完API文檔后才開(kāi)始寫代碼么?),直到我們到了某個(gè)測(cè)試階段,突然蹦出一個(gè)NullPointerException異常,我們才意識(shí)到原來(lái)我們得像下面這樣加一個(gè)判斷來(lái)搞定這個(gè)可能會(huì)返回的null值。
// main public class Test{ public static void main(String[] args) final MyEntity myEntity = getMyEntity(false); if(null != myEntity){ System.out.println(myEntity.getName()); }else{ System.out.println("ERROR"); } } }
仔細(xì)想想過(guò)去這么些年,咱們是不是都這樣干過(guò)來(lái)的?如果直到測(cè)試階段才能發(fā)現(xiàn)某些null導(dǎo)致的問(wèn)題,那么現(xiàn)在問(wèn)題就來(lái)了——在那些雍容繁雜、層次分明的業(yè)務(wù)代碼中到底還有多少null沒(méi)有被正確處理呢?
對(duì)于null的處理態(tài)度,往往可以看出一個(gè)項(xiàng)目的成熟和嚴(yán)謹(jǐn)程度。比如Guava早在JDK1.6之前就給出了優(yōu)雅的null處理方式,可見(jiàn)功底之深。
鬼魅一般的null阻礙我們進(jìn)步
如果你是一位聚焦于傳統(tǒng)面向?qū)ο箝_(kāi)發(fā)的Javaer,或許你已經(jīng)習(xí)慣了null帶來(lái)的種種問(wèn)題。但是早在許多年前,大神就說(shuō)了null這玩意就是個(gè)坑。
托尼.霍爾(你不知道這貨是誰(shuí)嗎?自己去查查吧)曾經(jīng)說(shuō)過(guò):“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ā)明null這事稱為價(jià)值連城的錯(cuò)誤。因?yàn)樵?965那個(gè)計(jì)算機(jī)的蠻荒時(shí)代,空引用太容易實(shí)現(xiàn),讓哥根本經(jīng)不住誘惑發(fā)明了空指針這玩意。”)。
然后,我們?cè)倏纯磏ull還會(huì)引入什么問(wèn)題。
看看下面這個(gè)代碼:
String address = person.getCountry().getProvince().getCity();
如果你玩過(guò)一些函數(shù)式語(yǔ)言(Haskell、Erlang、Clojure、Scala等等),上面這樣是一種很自然的寫法。用Java當(dāng)然也可以實(shí)現(xiàn)上面這樣的編寫方式。
但是為了完滿的處理所有可能出現(xiàn)的null異常,我們不得不把這種優(yōu)雅的函數(shù)編程范式改為這樣:
if (person != null) { Country country = person.getCountry(); if (country != null) { Province province = country.getProvince(); if (province != null) { address = province.getCity(); } } }
瞬間,高逼格的函數(shù)式編程Java8又回到了10年前。這樣一層一層的嵌套判斷,增加代碼量和不優(yōu)雅還是小事。更可能出現(xiàn)的情況是:在大部分時(shí)間里,人們會(huì)忘記去判斷這可能會(huì)出現(xiàn)的null,即使是寫了多年代碼的老人家也不例外。
上面這一段層層嵌套的 null 處理,也是傳統(tǒng)Java長(zhǎng)期被詬病的地方。如果以Java早期版本作為你的啟蒙語(yǔ)言,這種get->if null->return 的臭毛病會(huì)影響你很長(zhǎng)的時(shí)間(記得在某國(guó)外社區(qū),這被稱為:面向entity開(kāi)發(fā))。
利用Optional實(shí)現(xiàn)Java函數(shù)式編程
好了,說(shuō)了各種各樣的毛病,然后我們可以進(jìn)入新時(shí)代了。
早在推出Java SE 8版本之前,其他類似的函數(shù)式開(kāi)發(fā)語(yǔ)言早就有自己的各種解決方案。下面是Groovy的代碼:
String version = computer?.getSoundcard()?.getUSB()?.getVersion():"unkonwn";
Haskell用一個(gè) Maybe 類型類標(biāo)識(shí)處理null值。而號(hào)稱多范式開(kāi)發(fā)語(yǔ)言的Scala則提供了一個(gè)和Maybe差不多意思的Option[T],用來(lái)包裹處理null。
Java8引入了 java.util.Optional<T>來(lái)處理函數(shù)式編程的null問(wèn)題,Optional<T>的處理思路和Haskell、Scala類似,但又有些許區(qū)別。先看看下面這個(gè)Java代碼的例子:
public class Test { public static void main(String[] args) { final String text = "Hallo world!"; Optional.ofNullable(text)//顯示創(chuàng)建一個(gè)Optional殼 .map(Test::print) .map(Test::print) .ifPresent(System.out::println); Optional.ofNullable(text) .map(s ->{ System.out.println(s); return s.substring(6); }) .map(s -> null)//返回 null .ifPresent(System.out::println); } // 打印并截取str[5]之后的字符串 private static String print(String str) { System.out.println(str); return str.substring(6); } } //Consol 輸出 //num1:Hallo world! //num2:world! //num3: //num4:Hallo world!
(可以把上面的代碼copy到你的IDE中運(yùn)行,前提是必須安裝了JDK8。)
上面的代碼中創(chuàng)建了2個(gè)Optional,實(shí)現(xiàn)的功能基本相同,都是使用Optional作為String的外殼對(duì)String進(jìn)行截?cái)嗵幚?。?dāng)在處理過(guò)程中遇到null值時(shí),就不再繼續(xù)處理。我們可以發(fā)現(xiàn)第二個(gè)Optional中出現(xiàn)s->null之后,后續(xù)的ifPresent不再執(zhí)行。
注意觀察輸出的 //num3:,這表示輸出了一個(gè)”"字符,而不是一個(gè)null。
Optional提供了豐富的接口來(lái)處理各種情況,比如可以將代碼修改為:
public class Test { public static void main(String[] args) { final String text = "Hallo World!"; System.out.println(lowerCase(text));//方法一 lowerCase(null, System.out::println);//方法二 } private static String lowerCase(String str) { return Optional.ofNullable(str).map(s -> s.toLowerCase()).map(s->s.replace("world", "java")).orElse("NaN"); } private static void lowerCase(String str, Consumer<String> consumer) { consumer.accept(lowerCase(str)); } } //輸出 //hallo java! //NaN
這樣,我們可以動(dòng)態(tài)的處理一個(gè)字符串,如果在任何時(shí)候發(fā)現(xiàn)值為null,則使用orElse返回預(yù)設(shè)默認(rèn)的“NaN”。
總的來(lái)說(shuō),我們可以將任何數(shù)據(jù)結(jié)構(gòu)用Optional包裹起來(lái),然后使用函數(shù)式的方式對(duì)他進(jìn)行處理,而不必關(guān)心隨時(shí)可能會(huì)出現(xiàn)的null。
我們看看前面提到的Person.getCountry().getProvince().getCity()怎么不用一堆if來(lái)處理。
第一種方法是不改變以前的entity:
import java.util.Optional; public class Test { public static void main(String[] args) { System.out.println(Optional.ofNullable(new Person()) .map(x->x.country) .map(x->x.provinec) .map(x->x.city) .map(x->x.name) .orElse("unkonwn")); } } class Person { Country country; } class Country { Province provinec; } class Province { City city; } class City { String name; }
這里用Optional作為每一次返回的外殼,如果有某個(gè)位置返回了null,則會(huì)直接得到”unkonwn”。
第二種辦法是將所有的值都用Optional來(lái)定義:
import java.util.Optional; public class Test { public static void main(String[] args) { System.out.println(new Person() .country.flatMap(x -> x.provinec) .flatMap(Province::getCity) .flatMap(x -> x.name) .orElse("unkonwn")); } } class Person { Optional<Country> country = Optional.empty(); } class Country { Optional<Province> provinec; } class Province { Optional<City> city; Optional<City> getCity(){//用于:: return city; } } class City { Optional<String> name; }
第一種方法可以平滑的和已有的JavaBean、Entity或POJA整合,而無(wú)需改動(dòng)什么,也能更輕松的整合到第三方接口中(例如spring的bean)。建議目前還是以第一種Optional的使用方法為主,畢竟不是團(tuán)隊(duì)中每一個(gè)人都能理解每個(gè)get/set帶著一個(gè)Optional的用意。
Optional還提供了一個(gè)filter方法用于過(guò)濾數(shù)據(jù)(實(shí)際上Java8里stream風(fēng)格的接口都提供了filter方法)。例如過(guò)去我們判斷值存在并作出相應(yīng)的處理:
if(Province!= null){ City city = Province.getCity(); if(null != city && "guangzhou".equals(city.getName()){ System.out.println(city.getName()); }else{ System.out.println("unkonwn"); } }
現(xiàn)在我們可以修改為
Optional.ofNullable(province) .map(x->x.city) .filter(x->"guangzhou".equals(x.getName())) .map(x->x.name) .orElse("unkonw");
到此,利用Optional來(lái)進(jìn)行函數(shù)式編程介紹完畢。Optional除了上面提到的方法,還有orElseGet、orElseThrow等根據(jù)更多需要提供的方法。orElseGet會(huì)因?yàn)槌霈F(xiàn)null值拋出空指針異常,而orElseThrow會(huì)在出現(xiàn)null時(shí),拋出一個(gè)使用者自定義的異常??梢圆榭碅PI文檔來(lái)了解所有方法的細(xì)節(jié)。
寫在最后的
Optional只是Java函數(shù)式編程的冰山一角,需要結(jié)合lambda、stream、Funcationinterface等特性才能真正的了解Java8函數(shù)式編程的效用。本來(lái)還想介紹一些Optional的源碼和運(yùn)行原理的,但是Optional本身的代碼就很少、API接口也不多,仔細(xì)想想也沒(méi)什么好說(shuō)的就省略了。
Optional雖然優(yōu)雅,但是個(gè)人感覺(jué)有一些效率問(wèn)題,不過(guò)還沒(méi)去驗(yàn)證。如果有誰(shuí)有確實(shí)的數(shù)據(jù),請(qǐng)告訴我。
本人也不是“函數(shù)式編程支持者”。從團(tuán)隊(duì)管理者的角度來(lái)說(shuō),每提升一點(diǎn)學(xué)習(xí)難度,人員的使用成本和團(tuán)隊(duì)交互成本就會(huì)更高一些。就像在傳說(shuō)中Lisp可以比C++的代碼量少三十倍、開(kāi)發(fā)更高效,但是若一個(gè)國(guó)內(nèi)的常規(guī)IT公司真用Lisp來(lái)做項(xiàng)目,請(qǐng)問(wèn)去哪、得花多少錢弄到這些用Lisp的哥們啊?
但是我非常鼓勵(lì)大家都學(xué)習(xí)和了解函數(shù)式編程的思路。尤其是過(guò)去只侵淫在Java這一門語(yǔ)言、到現(xiàn)在還不清楚Java8會(huì)帶來(lái)什么改變的開(kāi)發(fā)人員,Java8是一個(gè)良好的契機(jī)。更鼓勵(lì)把新的Java8特性引入到目前的項(xiàng)目中,一個(gè)長(zhǎng)期配合的團(tuán)隊(duì)以及一門古老的編程語(yǔ)言都需要不斷的注入新活力,否則不進(jìn)則退。
以上就是對(duì)Java Optional 的資料整理,后續(xù)繼續(xù)補(bǔ)充相關(guān)資料,謝謝大家對(duì)本站的支持!
- 利用Java8 Optional如何避免空指針異常詳解
- 使用Java8中Optional機(jī)制的正確姿勢(shì)
- Java8中Optional的一些常見(jiàn)錯(cuò)誤用法總結(jié)
- JAVA8如何妙用Optional解決NPE問(wèn)題詳解
- Java函數(shù)式編程(六):Optional
- java8中forkjoin和optional框架使用
- Java Optional實(shí)踐(小結(jié))
- JAVA Optional類用法分享
- Java8中新特性O(shè)ptional、接口中默認(rèn)方法和靜態(tài)方法詳解
- Java如何使用Optional與Stream取代if判空邏輯(JDK8以上)
相關(guān)文章
深入理解Java基礎(chǔ)之try-with-resource語(yǔ)法糖
這篇文章主要介紹了深入理解Java基礎(chǔ)之try-with-resource語(yǔ)法糖,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-02-02網(wǎng)關(guān)Spring Cloud Gateway HTTP超時(shí)配置問(wèn)題
這篇文章主要介紹了網(wǎng)關(guān)Spring Cloud Gateway HTTP超時(shí)配置問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01淺析SpringBoot統(tǒng)一返回結(jié)果的實(shí)現(xiàn)
前后端開(kāi)發(fā)過(guò)程中數(shù)據(jù)交互規(guī)范化是一件非常重要的事情,不僅可以減少前后端交互過(guò)程中出現(xiàn)的問(wèn)題,也讓代碼邏輯更加具有條理,下面小編就和大家講講SpringBoot如何統(tǒng)一返回結(jié)果的吧2023-07-07Java下SpringBoot創(chuàng)建定時(shí)任務(wù)詳解
這篇文章主要介紹了Java下SpringBoot創(chuàng)建定時(shí)任務(wù)詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07SpringCloud之Admin服務(wù)監(jiān)控實(shí)現(xiàn)流程示例詳解
這篇文章主要為大家介紹了SpringCloud之Admin服務(wù)監(jiān)控流程示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09