前言
空指針是我們最常見也最討厭的異常,為了防止空指針異常,你不得在代碼里寫大量的非空判斷。
java 8引入了一個新的optional類。用于避免空指針的出現,也無需在寫大量的if(obj!=null)
這樣的判斷了,前提是你得將數據用optional裝著,它就是一個包裹著對象的容器。
都說沒有遇到過空指針異常的程序員不是java程序員,null確實引發過很多問題。java 8中引入了一個叫做java.util.optional
的新類可以避免null引起的諸多問題。
我們看看一個null引用能導致哪些危害。首先創建一個類computer,結構如下圖所示:
當我們調用如下代碼會怎樣?
1
|
string version = computer.getsoundcard().getusb().getversion(); |
上述代碼看似是沒有問題的,但是很多計算機(比如,樹莓派)其實是沒有聲卡的,那么調用getsoundcard()
方法可定會拋出空指針異常了。
一個常規的但是不好的的方法是返回一個null引用來表示計算機沒有聲卡,但是這就意味著會對一個空引調用getusb()方法,顯然會在程序運行過程中拋出控制異常,從而導致程序停止運行。想想一下,當你的程序在客戶端電腦上運行時,突然出現這種錯是多尷尬的一件事?
偉大計算機科學tony hoare曾經寫到:"我認為null引用從1965年被創造出來導致了十億美元的損失。當初使用null引用對我最大的誘惑就是它實現起來方便。"
那么該怎么避免在程序運行時會出現空指針異常呢?你需要保持警惕,并且不斷檢查可能出現空指針的情況,就像下面這樣:
1
2
3
4
5
6
7
8
9
10
11
|
string version = "unknown" ; if (computer != null ) { soundcard soundcard = computer.getsoundcard(); if (soundcard != null ){ usb usb = soundcard.getusb(); if (usb != null ){ version = usb.getversion(); } } } |
然而,你可以看到上述代碼有太多的null檢查,整個代碼結構變得非常丑陋。但是我們又不得不通過這樣的判斷來確保系統運行時不會出現空指針。如果在我們的業務代碼中出現大量的這種空引用判斷簡直讓人惱火,也導致我們代碼的可讀性會很差。
如果你忘記檢查要給值是否為空,null引用也是存在很大的潛在問題。這篇文章我將證明使用null引用作為值不存在的表示是不好的方法。我們需要一個更好的表示值不存在的模型,而不是再使用null引用。
java 8引入了一個新類叫做java.util.optional<t>
,這個類的設計的靈感來源于haskell語言和scala語言。這個類可以包含了一個任意值,像下面圖和代碼表示的那樣。你可以把optional看做是一個有可能包含了值的值,如果optional不包含值那么它就是空的,下圖那樣。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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(){ ... } } |
上述代碼展現了一臺計算機有可能包換一個聲卡(聲卡是有可能存在也有可能不存在)。聲卡也是有可能包含一個usb端口的。這是一種改善方法,該模型可以更加清晰的反映一個被給定的值是可以不存在的。
但是該怎么處理optional<soundcard>
這個對象呢?畢竟,你想要獲取的是usb的端口號。很簡單,optional類包含了一些方法來處理值是否存在的狀況。和null引用相比optional類迫使你在你要做值是否相關處理,從而避免了空指針異常。
需要說明的是optional類并不是要取代null引用。相反地,是為了讓設計的api更容易被理解,當你看到一個函數的簽名時,你就可以判斷要傳遞給這個函數的值是不是有可能不存在。這就促使你要打開optional類來處理確實值的狀況了。
采用optional模式
啰嗦了這么多,來看一些代碼吧!我們先看一下怎么使用optional改寫傳統的null引用檢測后是什么樣子。在這邊文章的末尾你將會明白怎么使用optional。
1
2
3
4
|
string name = computer.flatmap(computer::getsoundcard) .flatmap(soundcard::getusb) .map(usb::getversion) .orelse( "unknown" ); |
創建optional對象
可以創建一個空的optional對象:
1
|
optional<soundcard> sc = optional.empty(); |
接下來是創建一個包含非null值的optional:
1
2
|
soundcard soundcard = new soundcard(); optional<soundcard> sc = optional.of(soundcard); |
如果聲卡null,空指針異常會立即被拋出(這比在獲取聲卡屬性時才拋出要好)。
通過使用ofnullable,你可以創建一個可能包含null引用的optional對象:
1
|
optional<soundcard> sc = optional.ofnullable(soundcard); |
如果聲卡是null 引用,optional對象就是一個空的。
對optional中的值的處理
既然現在已經有了optional對象,你可以調用相應的方法來處理optional對象中的值是否存在。和進行null檢測相比,我們可以使用ifpresent()方法,像下面這樣:
1
2
|
optional<soundcard> soundcard = ...; soundcard.ifpresent(system.out::println); |
這樣就不必再做null檢測,如果optional對象是空的,那么什么信息將不會打印出來。
你也可以使用ispresent()
方法查看optional對象是否真的存在。另外,還有一個get()方法可以返回optional對象中的包含的值,如果存在的話。否則會拋出一個nosuchelementexception異常。這兩個方式可以像下面這樣搭配起來使用,從而避免異常:
1
2
3
|
if (soundcard.ispresent()){ system.out.println(soundcard.get()); } |
但是這種方式不推薦使用(它和null檢測相比沒有什么改進),下面我們將會探討一下工作慣用的方式。
返回默認值和相關操作
當遇到null時一個常規的操作就是返回一個默認值,你可以使用三元表達式來實現:
1
|
soundcard soundcard = maybesoundcard != null ? maybesoundcard : new soundcard( "basic_sound_card" ); |
使用optional對象的話,你可以orelse()
使用重寫,當optional是空的時候orelse()
可以返回一個默認值:
1
|
soundcard soundcard = maybesoundcard.orelse( new soundcard( "defaut" )); |
類似地,當optional為空的時候也可以使用orelsethrow()拋出異常:
1
2
|
soundcard soundcard = maybesoundcard.orelsethrow(illegalstateexception:: new ); |
使用filter過濾特定的值
我們常常會調用一個對象的方法來判斷它的一下屬性。比如,你可能需要檢測usb端口號是否是某個特定值。為了安全起見,你需要檢查指向usb的醫用是否是null,然后再調用getversion()
方法,像下面這樣:
1
2
3
4
|
usb usb = ...; if (usb != null && "3.0" .equals(usb.getversion())){ system.out.println( "ok" ); } |
如果使用optional的話可以使用filter函數重寫:
1
2
3
|
optional<usb> maybeusb = ...; maybeusb.filter(usb -> "3.0" .equals(usb.getversion()) .ifpresent(() -> system.out.println( "ok" )); |
filter方法需要一個predicate對向作為參數。如果optional中的值存在并且滿足predicate,那么filter函數將會返回滿足條件的值;否則,會返回一個空的optional對象。
使用map方法進行數據的提取和轉化
一個常見的模式是提取一個對象的一些屬性。比如,對于一個soundcard對象,你可能需要獲取它的usb對象,然后判斷它的的版本號。通常我們的實現方式是這樣的:
1
2
3
4
5
6
|
if (soundcard != null ){ usb usb = soundcard.getusb(); if (usb != null && "3.0" .equals(usb.getversion()){ system.out.println( "ok" ); } } |
我們可以使用map方法重寫這種檢測null,然后再提取對象類型的對象。
1
|
optional<usb> usb = maybesoundcard.map(soundcard::getusb); |
這個和使用stream的map函數式一樣的。使用stream需要給map函數傳遞一個函數作為參數,這個傳遞進來的函數將會應用于stream中的每個元素。當stream時空的時候,什么也不會發生。
optional中包含的值將會被傳遞進來的函數轉化(這里是一個從聲卡中獲取usb的函數)。如果optional對象時空的,那么什么也不會發生。
然后,我們結合map方法和filter方法過濾掉usb的版本號不是3.0的聲卡。
1
2
3
|
maybesoundcard.map(soundcard::getusb) .filter(usb -> "3.0" .equals(usb.getversion()) .ifpresent(() -> system.out.println( "ok" )); |
這樣我們的代碼開始變得像有點像開始我們給出的樣子,沒有了null檢測。
使用flatmap函數傳遞optional對象
現在已經介紹了一個可以使用optional重構代碼的例子,那么我們應該如何使用安全的方式實現下面代碼呢?
1
|
string version = computer.getsoundcard().getusb().getversion(); |
注意上面的代碼都是從一個對象中提取另一個對象,使用map函數可以實現。在前面的文章中我們設置了computer中包含的是一個optional<soundcard>
對象,soundcard包含的是一個optional<usb>
對象,因此我們可以這么重構代碼
1
2
3
4
|
string version = computer.map(computer::getsoundcard) .map(soundcard::getusb) .map(usb::getversion) .orelse( "unknown" ); |
不幸的是,上面的代碼會編譯錯誤,那么為什么呢?computer變量是optional<computer>
類型的,所以它調用map函數是沒有問題的。但是getsoundcard()
方法返回的是一個optional<soundcard>
的對象,返回的是optional<optional<soundcard>>
類型的對象,進行了第二次map函數的調用,結果調用getusb()
函數就變成非法的了。
下面的圖描述了這種場景:
map函數的源碼實現是這樣的:
1
2
3
4
5
6
7
8
|
public <u> optional<u> map(function<? super t, ? extends u> mapper) { objects.requirenonnull(mapper); if (!ispresent()) return empty(); else { return optional.ofnullable(mapper.apply(value)); } } |
可以看出map函數還會再調用一次optional.ofnullable()
, 從而導致返回optional<optional<soundcard>>
optional提供了flatmap這個函數,它的設計意圖是當對optional對象的值進行轉化(就像map操作)然后一個兩級optional壓縮成一個。下面的圖展示了optional對象通過調用map和flatmap進行類型轉化的不同:
因此我們可以這樣寫:
1
2
3
4
|
string version = computer.flatmap(computer::getsoundcard) .flatmap(soundcard::getusb) .map(usb::getversion) .orelse( "unknown" ); |
第一個flatmap保證了返回的是optional<soundcard>
而不是optional<optional<soundcard>>
,第二個flatmap實現了同樣的功能從而返回的是 optional<usb>
。注意第三次調用了map()
,因為getversion()
返回的是一個string對象而不是一個optional對象。
我們終于把剛開始使用的嵌套null檢查的丑陋代碼改寫了可讀性高的代碼,也避免了空指針異常的出現的代碼。
總結
在這片文章中我們采用了java 8提供的新類java.util.optional<t>
。這個類的初衷不是要取代null引用,而是幫助設計者設計出更好的api,只要讀到函數的簽名就可知道該函數是否接受一個可能存在也可能不存在的值。另外,optional迫使你去打開optional,然后處理值是否存在,這就使得你的代碼避免了潛在的空指針異常。
好了,以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對服務器之家的支持。
原文鏈接:http://www.imooc.com/article/22666