在Apple發布的Swift語言中,有些變數的型態名稱之後會加上問號(像是Int?),乍看很新奇,引發了不少討論,實際上這是個Optional型態,JDK8的Lambda專案中,API不少都會搭配Optional,隨著越來越多人開始評估JDK8,也有了越來越多對Optional的討論,實際上Optional不是新的東西,然而,對Java這門語言來說,或許是個遲到的東西。

可有可無的Optional

不管在哪個語言中出現Optional之類的東西,基本上都是代表著可能出現值,也可能不出現值,其目的多半是為了處理語言中null類似物所引發的問題,我在先前專欄〈補救null的策略〉中曾經對此做過討論,其中解法之一就是使用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型態的變數,也是可以指向null,因而,在Java上,Optional的使用就相當地具有選擇性,或者是說值得討論。

選擇性地使用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>>而不易閱讀與使用。

既有的API會存在銜接問題,例如字串,雖然本身空字串""的概念,但是過去很多情況下,開發者在沒有結果而傳回型態是字串時,習慣傳回null,這時可選擇統一傳回""(或使用nullToEmpty來轉換)或Optional<String>;在允許null的容器中,思考用其他結構來取代,例如若有List<String>中允許null,那麼考慮用Map<Integer, String>來取代,而不一定要將List<String>中的null改置入Optional.empty(),使得容器型態成為List<Optional<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。

作者簡介


Advertisement

更多 iThome相關內容