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