摘要
空閑時會抽空學習同在jvm上運行的Groovy和Scala,發現他們對null的處理比早期版本Java慎重很多。在Java8中,Optional為函數式編程的null處理給出了非常優雅的解決方案。本文將說明長久以來Java中對null的蹩腳處理,然后介紹使用Optional來實現Java函數式編程。
那些年困擾著我們的null
在Java江湖流傳著這樣一個傳說:直到真正了解了空指針異常,才能算一名合格的Java開發人員。在我們逼格閃閃的java碼字符生涯中,每天都會遇到各種null的處理,像下面這樣的代碼可能我們每天都在反復編寫:
1
2
3
4
5
|
if ( null != obj1){ if ( null != obje2){ // do something } } |
稍微有點眼界javaer就去干一些稍有逼格的事,弄一個判斷null的方法:
1
2
3
4
5
6
7
8
9
10
11
|
boolean checkNotNull(Object obj){ return null == obj ? false : true ; } void do (){ if (checkNotNull(obj1)){ if (checkNotNull(obj2)){ //do something } } } |
然后,問題又來了:如果一個null表示一個空字符串,那”"表示什么?
然后慣性思維告訴我們,”"和null不都是空字符串碼?索性就把判斷空值升級了一下:
1
2
3
4
5
6
7
8
9
10
|
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編碼風格編寫:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
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 ; } } } |
這一段代碼很簡單,日常的業務代碼肯定比這個復雜的多,但是實際上我們大量的Java編碼都是按這種套路編寫的,懂貨的人一眼就可以看出最終肯定會拋出NullPointerException。但是在我們編寫業務代碼時,很少會想到要處理這個可能會出現的null(也許API文檔已經寫得很清楚在某些情況下會返回null,但是你確保你會認真看完API文檔后才開始寫代碼么?),直到我們到了某個測試階段,突然蹦出一個NullPointerException異常,我們才意識到原來我們得像下面這樣加一個判斷來搞定這個可能會返回的null值。
1
2
3
4
5
6
7
8
9
10
11
|
// 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" ); } } } |
仔細想想過去這么些年,咱們是不是都這樣干過來的?如果直到測試階段才能發現某些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異常,我們不得不把這種優雅的函數編程范式改為這樣:
1
2
3
4
5
6
7
8
9
|
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<T>來處理函數式編程的null問題,Optional<T>的處理思路和Haskell、Scala類似,但又有些許區別。先看看下面這個Java代碼的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
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提供了豐富的接口來處理各種情況,比如可以將代碼修改為:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
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來定義:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
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方法)。例如過去我們判斷值存在并作出相應的處理:
1
2
3
4
5
6
7
8
|
if (Province!= null ){ City city = Province.getCity(); if ( null != city && "guangzhou" .equals(city.getName()){ System.out.println(city.getName()); } else { System.out.println( "unkonwn" ); } } |
現在我們可以修改為
1
2
3
4
5
|
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特性引入到目前的項目中,一個長期配合的團隊以及一門古老的編程語言都需要不斷的注入新活力,否則不進則退。
以上就是對Java Optional 的資料整理,后續繼續補充相關資料,謝謝大家對本站的支持!