iThome 網站首載:遲到的Optional?
在Apple發佈的Swift語言中,有些變數的型態名稱之後會加上問號(像是
Int?
),乍看很新奇,引發了不少討論,實際上這是個Optional
型態,JDK8的Lambda專案中,API不少都會搭配Optional
,隨著越來越多人開始評估JDK8,也有了越來越多對Optional
的討論,實際上Optional
不是新的東西,然而,對Java這門語言來說,或許是個遲到的東西。- 可有可無的Optional
對於一開始就內建
Optional
語法支援或API的語言來說,像是Swift,由於Optional
的影子隨時隨地都會出現(也因此成了Swift中討論的熱點特性),提醒了開發者必須處理可能出現或不出現值的狀況,這類語言使用接受Optional
的程度就較高,也較少有null引發的相關問題,像是Scala一開始就有Option
,因而較少會面對NullPointerException
的問題。實際上,在JDK8出現前,就有程式庫提供
Optional
,像是guava-libraries,或許因為並非Java標準化API,因而不被重視,JDK8對Optional
做了標準化,因而也就引來了一些爭論:該在會出現null
可能性的場合,全面使用Optional
取代嗎?實際上,對既有的程式來說,這樣的改動在API規格上更動太大,與Java一些使用慣例也有可能產生衝突問題,像是Getter慣例,因而全面採用不切實際。對JDK8來說,
Optional
一開始的設計,也不是用來取代所有可能出現null
或引發NullPointerException
的場合,在〈Shouldn't Optional be Serializable?〉這篇討論中,Brian Goetz解釋了為什麼Optional
不是Serializable
,因為當初的設計是應當只將Optional
用在傳回值時使用,甚至一度想將之命名為OptionalReturn
,目的是從名稱上強調、表示出它的意圖與使用場合。顯然地,Optional
不適合當作值域(Field),因為它不是Serializable
,開發者也沒辦法禁止Java的變數指向null
,因而想用Optional
來全面避免NullPointerException
也是不可能的事。- 選擇性地使用Optional
在選擇是否使用
Optional
上,經常被討論的就是參數上該使用Optional
嗎?這方面爭議最大,因為方法呼叫上並不方便,畢竟使用者得特別將值包裝為Optional
才能作為引數,即使在Scala中一開始就內建了Option
,是否在參數上使用Option
也一直是有爭議;另一方面,某些參數可有可無的目的,通常是在參數值從缺時提供預設值,這在Java中可以用Overloading來解決,也可以避免在方法中使用不必要的防禦式設計來檢查傳入值為null
的情況,必要時也可搭配FindBugs的@DefaultAnnotation(NonNull.class)
或@NonNull
來做靜態檢查。在方法中使用
Optional.ofNullable
來取代那些原本會檢查null
的程式碼,比較不會有爭議,通常有也只是閱讀習慣的問題,畢竟終究是會導致一點風格轉變,這部份不否認地,程式碼相關維護人員都得學習與習慣Optional
的使用,大家才能從風格轉變上得到好處,用map
或flatMap
來取代連續的null
判斷也會是後續的好處,要瞭解、熟悉與閱讀這兩個方法的運用,並不會花太多時間。將
Optional
作為傳回值,一般是比較建議的,JDK8上Optional
的設計原本也是為此,傳回值型態上,如果開發者想要明確提示API客戶端,必須檢查結果可能是空的情況時,可能就是使用Optional
的時機,搭配有方法提示功能的編譯器時,更能突顯益處。不過,並非所有結果為空的情況,都要改為Optional
,像是對於那些本身有定義「空」或「無值」的API,像是Collection
,這些API在沒有結果時,應該傳回本身定義的「空」,例如Collections.emptyList()
,這也可避免傳回值宣告為Optional<Collection<String>>
而不易閱讀與使用。- Null Object Pattern
在避免
NullPointerException
上,其實尚有其他方式可以避免,像是採用Null Object Pattern,這個模式可以在小幅更動API的情況下,避免NullPointerException
且讓程式碼變得簡潔,具體來說,就是為某型態建立一個子類別,例如為Customer
建立一個NullCustomer
子類別,實現檢查出null
時的一些預設行為,然後用NullCustomer
實例取代那些原本傳入null
的場合,並去除掉那些null
檢查,使得程式碼簡潔,NullCustomer
實例可以設計為單例(Singleton),以像是Customer.NullCustomer
或Customer.nullCustomer()
的方式提供。實際上,這就是在後續為事前沒有定義「空」或「無值」的API進行補救定義,將原本空或無值時該有的特定行為封裝到特定的Null Object中,有趣的是,Null Object發展出幾個特定的模式,例如,Groovy中如果撰寫p.job?.salary,p或job或salary其中之一是null,最後都會得到null的結果,而不是得到NullPointerException,這稱為Safe Navigation Operator。
Groovy中還有個Elvis Operator,不同於Java只將
?:
作為三元運算子(Ternary operator),Groovy中的?:
可以是二元運算子(Binary operator),舉例來說,user.name ?: "Anonymous"
如果user.name
不為null
,就會傳回user.name
的值,否則傳回"Anonymous"
,相對於user.name ? user.name : "Anonymous"
來說簡潔許多。儘管沒有語法上的直接支援,JDK8中的Optional
,算是在API層次上支援了Elvis Operator或Safe Navigation Operator,例如,Groovy中user.name ?: "Anonymous"
,使用Optional
就會是userOptional.orElse("Anonymous")
,Groovy中p.job?.salary
,JDK8中也可以用Optional
的map
來達到類似需求。- 遲到的是處理不存在時的共識
Optional
不是新的東西,處理的問題也不是沒有過解決方案,只是始終未受重視,某些程度上,Swift中將Optional
處理內建為語法,或者是JDK8中對Optional
的標準化,起到了像設計模式這東西的作用,能在思考如何處理有無值這類議題時,有個共同的思考起點,喚起大家對這類議題的重視,從而討論出大家共同的基礎或共識,並能有共通的詞彙,而不是自我想像,沒有規範下,各自使用各自的想法來解決這類問題。實際上,除了明確定義「空」或「無」、值不存在時流程上該如何處理之外,還有更多的情況,像是結果不存在是錯誤還是空無,不存在這個問題,遠比我們想像地還要複雜,ingramchen在〈Java 8 Optional, Revisited〉最後就打趣的說,相對於這類要討論的情況「Monad真是簡單多了」。
Optional
在Java中的標準化算是遲到了,不過或許遲到的並不是Optional
,而是處理不存在時的共識,Optional
只是提醒了我們,過去對這類問題忽視的程度有多麼嚴重,若從今以後還不加以重視,即使用了Optional
,也仍不可能避免NullPointerException
,畢竟,開發者還是可以在宣告Optional
的地方持續漫不經心,繼續傳遞null
。