在Java江湖流傳著這樣一個傳說:直到真正了解了空指針異常,才能算一名合格的Java開發人員。在我們逼格閃閃的java碼字元生涯中,每天都會遇到各種null的處理,像下面這樣的程式碼可能我們每天都在反覆編寫:
if(null != obj1){ if(null != obje2){ // do something } }
稍微有點眼界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 } } }
有空的話各位可以看看目前專案中或是自己過往的程式碼,到底寫了多少和上面類似的程式碼。
不知道你是否認真思考過一個問題:一個null到底代表什麼?
淺顯的認知-null當然表示「值不存在」。
對記憶體管理有點經驗的理解-null表示記憶體沒有被分配,指標指向了一個空位址。
稍微透徹點的認知-null可能表示某個地方處理有問題了,也可能表示某個數值不存在。
被虐千萬次的認識--哎喲,又一個NullPointerException異常,看來我得加一個if(null != value)了。
回想一下,在咱們前面碼字生涯中到底遇到過多少次java.lang.NullPointerException異常? NullPointerException作為一個RuntimeException層級的異常不用顯示捕獲,若不小心處理我們經常會在生產日誌中看到各種由NullPointerException引起的異常堆疊輸出。而且根據這個異常堆疊資訊我們根本無法定位到導致問題的原因,因為並不是拋出NullPointerException的地方引發了這個問題。我們得更深處去查詢什麼地方產生了這個null,而這個時候日誌往往無法追蹤。
有時更悲劇的是,產生null值的地方往往不在我們自己的專案程式碼中。這就存在一個更尷尬的事實-在我們呼叫各種良莠不齊第三方介面時,說不清某個介面在某種機緣巧合的情況下就會回傳一個null…
回到前面對null的認知問題。很多javaer認為null就是表示「什麼都沒有」或「值不存在」。按照這個慣性思考我們的程式碼邏輯是:你調用我的接口,按照你給我的參數返回對應的“值”,如果這條件沒法找到對應的“值”,那我當然返回一個null給你表示沒有「任何東西」了。我們看看下面這個程式碼,用很傳統很標準的Java編碼風格編寫:
class MyEntity{ int id; String name; String getName(){ return name; } }// mainpublic 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; } } }
這一段程式碼很簡單,日常的業務程式碼肯定比這個複雜的多,但是實際上我們大量的Java編碼都是按這種套路寫的,懂貨的人一眼就可以看出最終一定會拋出NullPointerException。但是當我們編寫業務程式碼時,很少會想到要處理這個可能會出現的null(也許API文檔已經寫得很清楚在某些情況下會返回null,但是你確保你會認真看完API文檔後才會開始寫程式碼麼?
// mainpublic 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"); } } }
仔細想想過去這麼多年,咱們是不是都這樣幹過來的?如果直到測試階段才能發現某些null導致的問題,那麼現在問題就來了——在那些雍容繁雜、層次分明的業務代碼中到底還有多少null沒有被正確處理呢?
對於null的處理態度,往往可以看出一個專案的成熟和嚴謹程度。例如Guava早在JDK1.6之前就給了優雅的null處理方式,可見功底之深。
鬼魅一般的null阻礙我們進步
如果你是一位聚焦於傳統物件導向開發的Javaer,或許你已經習慣了null帶來的種種問題。但是早在許多年前,大神就說了null這玩意就是個坑。
托尼.霍爾(你不知道這貨是誰嗎?自己去查查吧)曾經說過:「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.」(大意是:「哥將發明null這事稱為價值連城的錯誤。因為在1965那個計算機的蠻荒時代,空引用的蠻荒時代,空引用太容易實現,讓哥根本經不住誘惑發明了空指針這玩意。
然後,我們再看看null還會引入什麼問題。
看看下面這個程式碼:
String address = person.getCountry().getProvince().getCity();
如果你玩过一些函数式语言(Haskell、Erlang、Clojure、Scala等等),上面这样是一种很自然的写法。用Java当然也可以实现上面这样的编写方式。
但是为了完满的处理所有可能出现的null异常,我们不得不把这种优雅的函数编程范式改为这样:
if (person != null) { Country country = person.getCountry(); if (country != null) { Province province = country.getProvince(); if (province != null) { address = province.getCity(); } } }
瞬间,高逼格的函数式编程Java8又回到了10年前。这样一层一层的嵌套判断,增加代码量和不优雅还是小事。更可能出现的情况是:在大部分时间里,人们会忘记去判断这可能会出现的null,即使是写了多年代码的老人家也不例外。
上面这一段层层嵌套的 null 处理,也是传统Java长期被诟病的地方。如果以Java早期版本作为你的启蒙语言,这种get->if null->return 的臭毛病会影响你很长的时间(记得在某国外社区,这被称为:面向entity开发)。
利用Optional实现Java函数式编程
好了,说了各种各样的毛病,然后我们可以进入新时代了。
早在推出Java SE 8版本之前,其他类似的函数式开发语言早就有自己的各种解决方案。下面是Groovy的代码:
String version = computer?.getSoundcard()?.getUSB()?.getVersion():"unkonwn";
Haskell用一个 Maybe 类型类标识处理null值。而号称多范式开发语言的Scala则提供了一个和Maybe差不多意思的Option[T],用来包裹处理null。
Java8引入了 java.util.Optional
public class Test { public static void main(String[] args) { final String text = "Hallo world!"; Optional.ofNullable(text)//显示创建一个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中运行,前提是必须安装了JDK8。)
上面的代码中创建了2个Optional,实现的功能基本相同,都是使用Optional作为String的外壳对String进行截断处理。当在处理过程中遇到null值时,就不再继续处理。我们可以发现第二个Optional中出现s->null之后,后续的ifPresent不再执行。
注意观察输出的 //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
这样,我们可以动态的处理一个字符串,如果在任何时候发现值为null,则使用orElse返回预设默认的"NaN"。
总的来说,我们可以将任何数据结构用Optional包裹起来,然后使用函数式的方式对他进行处理,而不必关心随时可能会出现的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的使用方法为主,毕竟不是团队中每一个人都能理解每个get/set带着一个Optional的用意。
Optional还提供了一个filter方法用于过滤数据(实际上Java8里stream风格的接口都提供了filter方法)。例如过去我们判断值存在并作出相应的处理:
if(Province!= null){ City city = Province.getCity(); if(null != city && "guangzhou".equals(city.getName()){ System.out.println(city.getName()); }else{ System.out.println("unkonwn"); } }
现在我们可以修改为
Optional.ofNullable(province) .map(x->x.city) .filter(x->"guangzhou".equals(x.getName())) .map(x->x.name) .orElse("unkonw");
到此,利用Optional来进行函数式编程介绍完毕。Optional除了上面提到的方法,还有orElseGet、orElseThrow等根据更多需要提供的方法。orElseGet会因为出现null值抛出空指针异常,而orElseThrow会在出现null时,抛出一个使用者自定义的异常。可以查看API文档来了解所有方法的细节。
写在最后的
Optional只是Java函数式编程的冰山一角,需要结合lambda、stream、Funcationinterface等特性才能真正的了解Java8函数式编程的效用。本来还想介绍一些Optional的源码和运行原理的,但是Optional本身的代码就很少、API接口也不多,仔细想想也没什么好说的就省略了。
Optional虽然优雅,但是个人感觉有一些效率问题,不过还没去验证。如果有谁有确实的数据,请告诉我。
本人也不是「函數式程式設計支持者」。從團隊管理者的角度來說,每提升一點學習難度,人員的使用成本和團隊互動成本就會更高。就像在傳說中Lisp可以比C++的程式碼量少三十倍、開發更有效率,但是若一個國內的常規IT公司真用Lisp來做項目,請問去哪、得花多少錢弄到這些用Lisp的哥們啊?
但是我非常鼓勵大家都學習並了解函數式程式設計的想法。尤其是過去只侵淫在Java這一門語言、到現在還不清楚Java8會帶來什麼改變的開發人員,Java8是一個好的契機。更鼓勵把新的Java8特性引入目前的專案中,一個長期配合的團隊以及一門古老的程式語言都需要不斷的注入新活力,否則不進則退。