概述:Optional最早是Google公司Guava中的概念,代表的是可選值。Optional類從Java8版本開始加入豪華套餐,主要為了解決程序中的NPE問題,從而使得更少的顯式判空,防止代碼污染,另一方面,也使得領域模型中所隱藏的知識,得以顯式體現在代碼中。Optional類位於java.util包下,對鏈式編程風格有一定的支持。實際上,Optional更像是一個容器,其中存放的成員變量是一個T類型的value,可值可Null,使用的是Wrapper模式,對value操作進行了包裝與設計。本文將從Optional所解決的問題開始,逐層解剖,由淺入深,文中會出現Optioanl方法之間的對比,實踐,誤用情況分析,優缺點等。與大家一起,對這項Java8中的新特性,進行理解和深入。
1、解決的問題
臭名昭著的空指針異常,是每個程序員都會遇到的一種常見異常,任何訪問對象的方法與屬性的調用,都可能會出現NullPointException,如果要確保不觸發異常,我們通常需要進行對象的判空操作。
舉個栗子,有一個人(Shopper)進超市購物,可能會用購物車(Trolley)也可能會用其它方式,購物車裏可能會有一袋栗子(Chestnut),也可能沒有。三者定義的代碼如下:
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public class Shopper {
private Trolley trolley;
public Trolley getTrolley(){
return trolley;
}
}
public class Trolley {
private Chestnut chestnut;
public Chestnut getChestnut(){
return chestnut;
}
}
public class Chestnut {
private String name;
public String getName(){
return name;
}
}
這時想要獲得購物車中栗子的名稱,像下面這麼寫,就可能會見到我們的“老朋友”(NPE)
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public String result(Shopper shopper){
return shopper.getTrolley().getChestnut().getName();
}
為了能避免出現空指針異常,通常的寫法會逐層判空(多層嵌套法),如下
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public String result(Shopper shopper) {
if (shopper != null) {
Trolley trolley = shopper.getTrolley();
if (trolley != null) {
Chestnut chestnut = trolley.getChestnut();
if (chestnut != null) {
return chestnut.getName();
}
}
}
return "獲取失敗遼";
}
多層嵌套的方法在對象級聯關係比較深的時候會看的眼花繚亂的,尤其是那一層一層的括號;另外出錯的原因也因為缺乏對應信息而被模糊(例如trolley為空時也只返回了最後的獲取失敗。當然也可以在每一層增加return,相應的代碼有會變得很冗長),所以此時我們也可以用遇空則返回的衞語句進行改寫。
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public String result(Shopper shopper) {
if (shopper == null) {
return "購物者不存在";
}
Trolley trolley = shopper.getTrolley();
if (trolley == null) {
return "購物車不存在";
}
Chestnut chestnut = trolley.getChestnut();
if (chestnut == null) {
return "栗子不存在";
}
return chestnut.getName();
}
為了取一個名字進行了三次顯示判空操作,這樣的代碼當然沒有問題,但是優秀的工程師們總是希望能獲得更優雅簡潔的代碼。Optional就提供了一些方法,實現了這樣的期望。
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public String result(Shopper shopper){
return Optional.ofNullable(shopper)
.map(Shopper::getTrolley)
.map(Trolley::getChestnut)
.map(Chestnut::getName)
.orElse("獲取失敗遼");
}
2、常用方法
1)獲得Optional對象
Optional類中有兩個構造方法:帶參和不帶參的。帶參的將傳入的參數賦值value,從而構建Optional對象;不帶參的用null初始化value構建對象。
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
private Optional() {}
private Optional(T value) {}
但是兩者都是私有方法,而實際上Optional的對象都是通過靜態工廠模式的方式構建,主要有以下三個函數
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public static <T> Optional<T> of(T value) {}
public static <T> Optional<T> ofNullable(T value) {}
public static <T> Optional<T> empty() {}
創建一個一定不為空的Optional對象,因為如果傳入的參數為空會拋出NPE
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Chestnut chestnut = new Chestnut();
Optional<Chestnut> opChest = Optional.of(chestnut);
創建一個空的Optional對象
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Optional<Chestnut> opChest = Optional.empty();
創建一個可空的Optional對象
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Chestnut chestnut = null;
Optional<Chestnut> opChest = Optional.ofNullable(chestnut);
2)正常使用
正常使用的方法可以被大致分為三種類型,判斷類、操作類和取值類
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
//判斷類
public boolean isPresent() {}
//操作類
public void ifPresent(Consumer<? super T> consumer) {}
//取值類
public T get() {}
public T orElse(T other) {}
public T orElseGet(Supplier<? extends T> other) {}
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {}
isPresent()方法像一個安全閥,控制着容器中的value值是空還是有值,用法與原本的null != obj的用法相似。當obj有值返回true,為空返回false(即value值存在為真)。但一般實現判斷空或不為空的邏輯,使用Optional其他的方法處理會更為常見。如下代碼將會打印出沒有栗子的悲慘事實。
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Optional<Chestnut> opChest = Optional.empty();
if (!opChest.isPresent()){
System.out.println("容器裏沒有栗子");
}
ifPresent()方法是一個操作類的方法,他的參數是一段目標類型為Consumer的函數,當value不為空時,自動執行consumer中的accept()方法(傳入時實現),為空則不執行任何操作。比如下面這段代碼,我們傳入了一段輸出value的lamda表達式,打印出了“遷西板栗”。
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Optional<Chestnut> opChest = Optional.ofNullable(new Chestnut("遷西板栗"));
opChest.ifPresent(c -> System.out.println(c.getName()));
get()方法源碼如下,可以看出,get的作用是直接返回容器中的value。但如此粗暴的方法,使用前如果不判空,在value為空時,便會毫不留情地拋出一個異常。
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public T get() {
if (value == null) {
throw new NoSuchElementException("No value present");
}
return value;
}
三個orElse方法與get相似,也都屬於取值的操作。與get不同之處在於orElse方法不用額外的判空語句,撰寫邏輯時比較愉快。三個orElse的相同之處是當value不為空時都會返回value。當為空時,則另有各自的操作:orElse()方法會返回傳入的other實例(也可以為Supplier類型的函數);orElseGet()方法會自動執行Supplier類型實例的get()方法;orElseThrow()方法會拋出一個自定的異常。更具體的差別會在後面的方法對比中描述。
如下面這段代碼,展示了在沒有栗子的時候,如何吐出“信陽板栗”、“鎮安板栗”,以及拋出“抹油栗子”的警告。
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Optional<Chestnut> opChest = Optional.empty();
System.out.println(opChest.orElse(new Chestnut("信陽板栗")));
System.out.println(opChest.orElseGet(() -> new Chestnut("鎮安板栗")));
try {
opChest.orElseThrow(() -> new RuntimeException("抹油栗子呀"));
}catch (RuntimeException e){
System.out.println(e.getMessage());
}
3)進階使用
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public Optional<T> filter(Predicate<? super T> predicate) {}
public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {}
public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {}
filter()方法接受謂詞為Predicate類型的函數作為參數,如果value值不為空則自動執行predicate的test()方法(傳入時實現),來判斷是否滿足條件,滿足則會返回自身Optional,不滿足會返回空Optional;如果value值為空,則會返回自身Optional(其實跟空Optional也差不多)。如下代碼,第二句中篩選條件“邵店板栗”與opChest中的板栗名不符,沒有通過過濾。而第三句的篩選條件與opChest一致,所以最後打印出來的是“寬城板栗”。
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Optional<Chestnut> opChest = Optional.ofNullable(new Chestnut("寬城板栗"));
opChest.filter(c -> "邵店板栗".equals(c.getName())).ifPresent(System.out::println);
opChest.filter(c -> "寬城板栗".equals(c.getName())).ifPresent(System.out::println);
map()和flatmap()方法傳入的都是一個Function類型的函數,map在這裏翻譯為“映射”,當value值不為空時進行一些處理,返回的值是經過mapper的apply()方法處理後的Optional類型的值,兩個方法的結果一致,處理過程中會有差別。如下代碼,從opChest中獲取了板栗名後,重新new了一個板栗取名“邢台板栗”,並打印出來,兩者輸出一致,處理形式上有差異,這個在後面的方法對比中會再次説到。
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Optional<Chestnut> opChest = Optional.ofNullable(new Chestnut("邢台板栗"));
System.out.println(opChest.map(c -> new Chestnut(c.getName())));
System.out.println(opChest.flatMap(c -> Optional.ofNullable(new Chestnut(c.getName()))));
4)1.9新增
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public void ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction) {}
public Optional<T> or(Supplier<? extends Optional<? extends T>> supplier) {}
public Stream<T> stream() {}
JDK1.9中增加了三個方法:ifPresentOrElse()、or()和stream()方法。
1.8時,ifPresent()僅提供了if(obj != null)的方法,並未提供if(obj != null)else{}的操作,所以在1.9中增加了一個ifPresentElse()方法,提供了這方面的支持。該方法接收兩個參數Consumer和Runnable類型的函數,當value不為空,調用action的accept()方法,這點與ifPresent()一致,當value為空時,會調用emptyAction的run()方法,執行else語義的邏輯。如下面代碼,會打印出“木有栗子”的提示。
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Optional<Chestnut> opChest = Optional.empty();
opChest.ifPresentElse(c -> System.out.println(c.getName()),c -> System.out.println("木有栗子呀"));
or()方法是作為orElse()和orElseGet()方法的改進而出現的,使用方法一致,但後兩個方法在執行完成後返回的並非包裝值。如果需要執行一些邏輯並返回Optional時,可以使用or()方法。該方法傳入Supplier接口的實例,當value有值時直接返回自身Optional,當為空時,自動執行suuplier的get()方法,幷包裝成Optional返回,其源碼中包裝的語句如下:
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Optional<T> r = (Optional<T>) supplier.get();
return Objects.requireNonNull(r);
stream()方法則不用多説,是一個提供給流式編程使用的方法,功能上是一個適配器,將Optional轉換成Stream:沒有值返回一個空的stream,或者包含一個Optional的stream。其源碼如下:
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
if (!isPresent()) {
return Stream.empty();
} else {
return Stream.of(value);
}
3、方法對比和總結
Optional封裝的方法較多,選擇一個合適的方法的前提是要了解各自適用的場景和異同
1)創建方法的對比
由於構造器為私有方法,創建對象只能通過靜態工廠的方式創建。of()、ofNullable()和empty()方法是三個靜態方法。先上源碼:
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
//工廠方法
public static <T> Optional<T> of(T value) {
return new Optional<>(value);
}
public static <T> Optional<T> ofNullable(T value) {
return value == null ? empty() : of(value);
}
public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}
//構造方法
private Optional() {
this.value = null;
}
private Optional(T value) {
this.value = Objects.requireNonNull(value);
}
//靜態常量
private static final Optional<?> EMPTY = new Optional<>()
of()方法通過調用帶參構造,new出一個Optional對象,正常形參帶值是不會有問題的,但是當形參為空時,設置value前的Objects.requireNonNull()非空校驗,就會拋出一個異常,代碼如下:
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
requireNonNull()方法是java.util包下Objects類的一個方法,作用是檢查傳入的參數是否為空,為空會拋出一個NPE,在Optional類中用到的地方還有很多。所以只有確信構造Optional所傳入的參數不為空時才可使用of()方法。
與of()相對的還有一個ofNullable()方法,該方法允許接受null值構造Optional,當形參為null時,調用empty()方法,而empty()方法返回的是一個編譯期就確定的常量EMPTY,EMPTY取值是無參構造器創建對象,最終得到的是一個value為空的Optional對象。
2)使用方法的對比
2.2)中説到,正常使用的方法中有屬於取值類的方法,orElse()、orElseGet()和orElseThrow(),這三個方法在非空時均返回value,但是為空時的處理各不相同。先上源碼:
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public T orElse(T other) {
return value != null ? value : other;
}
public T orElseGet(Supplier<? extends T> other) {
return value != null ? value : other.get();
}
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
if (value != null) {
return value;
} else {
throw exceptionSupplier.get();
}
orElse()和orElseGet()方法最直觀的差異是形參的不同,看下面一段代碼:
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
//測試語句
Optional<Chestnut> opChest = Optional.ofNullable(new Chestnut("桐柏板栗"));
//Optional<Chestnut> opChest = Optional.empty();
opChest.orElse(print("orELse"));
opChest.orElseGet(()->print("orElseGet"));
//調用方法
private static Chestnut print(String method){
System.out.println("燕山板栗最好吃----"+method);
return new Chestnut("燕山板栗");
}
第一次,new出一個“桐柏板栗”的Optional,分別調用orElse()和orElseGet()方法,結果出現了兩行的“燕山板栗最好吃”的輸出,因為兩個方法在value不為null時都會執行形參中的方法;
第二次,通過empty()方法獲得一個空栗子的Optional,再調用orElse()和orElseGet()方法,結果居然還出現了一行“燕山板栗最好吃”的輸出。
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
第一次輸出:
燕山板栗最好吃----orELse
燕山板栗最好吃----orElseGet
第二次輸出:
燕山板栗最好吃----orELse
其原因是orElseGet()的參數是Supplier目標類型的函數,簡單來説,Suppiler接口類似Spring的懶加載,聲明之後並不會佔用內存,只有執行了get()方法之後,才會調用構造方法創建出對象,而orElse()是快加載,即使沒有調用,也會實際的運行。
這個特性在一些簡單的方法上差距不大,但是當方法是一些執行密集型的調用時,比如遠程調用,計算類或者查詢類方法時,會損耗一定的性能。
orElseThrow()方法與orElseGet()方法的參數都是函數類型的,這意味着這兩種方法都是懶加載,但針對於必須要使用異常控制流程的場景,orElseThrow()會更加合適,因為可以控制異常類型,使得相比NPE會有更豐富的語義。
3)其他方法的對比
a、map與filterMap
先上源碼:
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
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));
}
}
public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Objects.requireNonNull(mapper.apply(value));
}
}
map()與filterMap()的相同點是,都接受一個Function類型的函數,並且返回值都是Optional類型的數據。但是從源碼中我們也能看出:
首先,map()在返回時,使用了ofNullable()函數對返回值包了一層,這個函數在2.1)已經説過,是一個Optional的工廠函數,作用是將一個數據包裝成Optional;而filterMap()返回時只是做了非空校驗,在應用mapper.apply時就已經是一個Optional類型的對象。
其次,從簽名中也可以看出,map()的Function的輸出值是"? extends U",這意味着在mapper.apply()處理完成後,只要吐出一個U類型或者U類型的子類即可;而filterMap()的Functional的輸出值是“Optional\<U>”,則在mapper.apply()處理完成之後,返回的必須是一個Optional類型的數據。
b、ifPresent與ifPresentOrElse
ifPresentOrElse()方法是作為ifPresent()的改進方法出現的。先看源碼:
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public void ifPresent(Consumer<? super T> action) {
if (value != null) {
action.accept(value);
}
}
public void ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction) {
if (value != null) {
action.accept(value);
} else {
emptyAction.run();
}
}
從源碼中可以看出,ifPresentOrElse()參數增加了一個Runnable類型的函數emptyAction,在value != null時,都激活了action.accept()方法。只是當value == null時,ifPresentOrElse()方法還會調用emptyAction.run()方法。所以總的來説,jdk1.9加入ifPresentOrElse()方法,是作為ifPreset在if-else領域的補充出現的。
c、or與orElse
同樣作為改進的or()方法也是為了解決orElse系列方法的“小缺點”出現的,先看源碼:
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public Optional<T> or(Supplier<? extends Optional<? extends T>> supplier) {
Objects.requireNonNull(supplier);
if (isPresent()) {
return this;
} else {
@SuppressWarnings("unchecked")
Optional<T> r = (Optional<T>) supplier.get();
return Objects.requireNonNull(r);
}
}
public T orElse(T other) {
return value != null ? value : other;
}
public T orElseGet(Supplier<? extends T> supplier) {
return value != null ? value : supplier.get();
}
or()方法在簽名形式上更接近orElseGet(),即形參都是Supplier類型的函數,但是與其不同的是,or()方法在形參中,指定了Supplier返回的類型必須為Optional類型,且value的類型必須為T或者T的子類。orElse系列的方法,更像是一種消費的方法,從一個Optional的實例中“取出“value的值進入下一步操作,而or()方法則像是建造者模式,對value有一定的操作之後,重新吐出的還是Optional類型的數據,所以使用時可以串聯在一起,後一個or處理前一個or吐出的Optional。
4)“危險”方法的對比
這裏指的“危險”指的是會拋出異常,畢竟引進Optional類的目的就是去除對NPE的判斷,如果此時再拋出一個NPE或者其他的異常,沒有處理好就會為程序引入不小的麻煩。所以對Optional中可能拋出異常的方法做一個總結。
首先,最直觀的會拋出異常的方法就是of()方法,因為of方法會調用帶參構造創建實例,而帶參構造中有對value非空的檢查,如果空會拋出NPE異常;
其次,get()方法也是一個“危險”的方法,因為當不判空直接使用get取值時,會觸發get中NoSuchElementException異常;
再次,orElseThrow()方法也會拋出異常,但是這種異常屬於人為指定的異常,是為了使得異常情況的語義更加豐富,而人為設置的,是一種可控的異常;
最後,在一些方法中,設置了參數非空檢查(Objects.requireNonNull()),這種檢查會拋出NPE異常,除去已經提到的帶參構造器,還有filter、map、flatMap、or這四個方法,如果傳入的接口實例是Null值就會隨時引爆NPE。
4、誤用形式與Best practice
1)誤用形式
a、初始化為null
第一種誤用形式是給Optional類型的變量初始化的時候.Optional類型變量是默認不為空的,所以在取方法執行的時候才可以肆無忌憚"點"出去,如果在初始化的時候出現:
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Optional<Chestnut> chest = null;
並且不及時為chest賦值,則還是容易出現NPE,正確的初始化方式應該是:
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Optional<Chestnut> chest = Optional.empty();
b、簡單判空
第二種比較常見的誤用形式應該是使用isPresent()做簡單判空。原本的代碼如下:
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public String getName(Chestnut chestnut){
if(chestnut == null){
return "栗子不存在";
}else return chestnut.name();
}
代碼中,通過檢查chestnut == null來處理為空時的情況,簡單使用isPresent()方法判空的代碼如下:
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public String getName(Chestnut chestnut){
Optional<Chestnut> opChest = Optional.ofNullable(chestnut);
if(!opChest.isPresent()){
return "栗子不存在";
}else return opChest.getname();
}
醬嬸兒並沒有太大差別,所以在使用Optional時,首先應避免使用**Optional.isPresent()**來檢查實例是否存在,因為這種方式和**null!= obj**沒有區別也沒什麼意義。
c、簡單get
第三種比較常見的誤用形式是使用Optional.get()方式來獲取Optional中value的值,get()方法中對value==null的情況有拋出異常,所以應該在做完非空校驗之後再從get取值,或者十分確定value一定不為空,否則會出現NoSuchElementException的異常。相對的,如果不是很確信,則使用orElse(),orElseGet(),orElseThrow()獲得你的結果會更加合適。
d、作為屬性字段和方法參數
第四種誤用形式在初學Optional的時候容易碰到,當指定某個類中的屬性,或者方法的參數為Optional的時候,idea會給出如下提示:
Reports any uses of java.util.Optional\<T>, java.util.OptionalDouble, java.util.OptionalInt, java.util.OptionalLongor com.google.common.base.Optionalas the type for a field or parameter. Optional was designed to provide a limited mechanism for library method return types where there needed to be a clear way to represent "no result". Using a field with type java.util.Optionalis also problematic if the class needs to be Serializable, which java.util.Optionalis not.
大意是不建議如此使用Optional。第一,不建議使用Optional作為字段或參數,其設計是為庫方法返回類型提供一種有限的機制,而這種機制可以清晰的表示“沒有結果”的語義;第二,Optional沒有實現Serilazable,是不可被序列化的。
這種誤用方法比較明顯,復現和避免也比較簡單。但筆者還想就這兩個建議的原因做進一步的探究,所以查閲了一些資料,大體的原因如下:
第一個原因,為什麼不適合做屬性字段和方法參數?直白的説,就是麻煩。為了引入Optional,卻需要加入多段樣板代碼,比如判空操作。使得在不合適的位置使用Optional不僅沒有給我們帶來便利,反而約束了寫代碼的邏輯。
寫以下域模型代碼
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public class Chestnut {
private String firstName;
private Optional<String> midName = Optional.empty();
private String lastName;
public void setMidName(Optional<String> midName) {
this.midName = midName;
}
public String getFullName() {
String fullName = firstName;
if(midName != null) {
if(midName.isPresent()){
fullName = fullName.concat("." + midName.get());
}
return fullName.concat("." + lastName);
}
}
}
可見在setter方法中沒有對形參做相應的校驗,那麼則需要在使用的getFullName()方法中,增加對屬性midName的判空操作,因為完全可能通過setter方法使得屬性為null。如果把判空移到setter方法中,也並沒有減少判空,使得平白擠進了一段樣板代碼。另外在傳入方法時,也需要對原本的value包裝一層後再傳入,使得代碼顯得累贅了,如下:
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
chest.setMidName(Optional.empty());
chest.setMidName(Optional.of("阿慄"));
在屬性不為Optional的時候,如果給屬性賦值,需要使用“消費”操作,比如orElse(),取出值再賦給屬性,相比直接傳入String類型的值作為字段和形參可以減少這些步驟,後者反而更加合適。
第二個原因,為什麼沒有實現序列化?相關可以參見Java Lamda的專家組討論。
JDK在序列化上比較特殊,需要同時兼顧向前和向後兼容,比如在JDK7中序列化的對象應該能夠在JDK8中反序列化,反之亦然。並且,序列化依賴於對象的identity保持唯一性。當前Optional是引用類型的,但其被標記為value-based class(基於值的類),並且有計劃在今後的某一個JDK版本中實現為value-based class,可見上圖。如果被設計為可序列化,就將出現兩個矛盾點:1)如果Optional可序列化,就不能將Optional實現為value-based class,而必須是引用類型,2)否則將value-based class加入同一性的敏感操作(包含引用的相等性如:==,同一性的hashcode或者同步等),但是這個與當前已發佈的JDK版本都是衝突的。所以綜上,考慮到未來JDK的規劃和實現的衝突,一開始就將Optional設置為不可序列化的,應該是最合適的方案了。
Value-Based Classes(基於值的類),以下是來自Java doc的解釋:
Value-based Classes
Some classes, such as java.util.Optionaland java.time.LocalDateTime, are value-based. Instances of a value-based class:
1、are final and immutable (though may contain references to mutable objects);
2、have implementations of equals, hashCode, and toStringwhich are computed solely from the instance's state and not from its identity or the state of any other object or variable;
3、make no use of identity-sensitive operations such as reference equality (==) between instances, identity hash code of instances, or synchronization on an instances's intrinsic lock;
4、are considered equal solely based on equals(), not based on reference equality (==);
5、do not have accessible constructors, but are instead instantiated through factory methods which make no committment as to the identity of returned instances;
6、are freely substitutable when equal, meaning that interchanging any two instances xand ythat are equal according to equals()in any computation or method invocation should produce no visible change in behavior.
A program may produce unpredictable results if it attempts to distinguish two references to equal values of a value-based class, whether directly via reference equality or indirectly via an appeal to synchronization, identity hashing, serialization, or any other identity-sensitive mechanism. Use of such identity-sensitive operations on instances of value-based classes may have unpredictable effects and should be avoided.
2)Best practice
實踐中常常組合使用以上各種方法,且很多方法常與Lambda表達式結合,獲取想要的結果,這裏列出兩個常見的使用方式,值類型轉換和集合元素過濾。
a、示例一
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Chestnut chestnut = new Chestnut("錐慄板栗");
if(chestnut != null){
String chestName = chestnut.getName();
if(chestName != null){
return chestName.concat("好好吃!");
}
}else{
return null;
}
可以簡化成:
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Chestnut chestnut = new Chestnut("錐慄板栗");
return Optional.ofNullable(chestnut)
.map(Chestnut::getName)
.map(name->name.concat("好好吃!"))
.orElse(null);
b、示例二
P lainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public static void main(String[] args) {
// 創建一個栗子集合
List<Chestnut> chestList = new ArrayList<>();
// 創建幾個栗子
Chestnut chest1 = new Chestnut("abc");
Chestnut chest2 = new Chestnut("efg");
Chestnut chest3 = null;
// 將栗子加入集合
chestList.add(chest1);
chestList.add(chest2);
chestList.add(chest3);
// 創建用於存儲栗子名的集合
List<String> nameList = new ArrayList();
// 循環栗子列表獲取栗子信息,值獲取不為空且栗子名以‘a’開頭
// 如果不符合條件就設置默認值,最後將符合條件的栗子名加入栗子名集合
for (Chestnut chest : chestList) {
nameList.add(Optional.ofNullable(chest)
.map(Chestnut::getName)
.filter(value -> value.startsWith("a"))
.orElse("未填寫"));
}
// 輸出栗子名集合中的值
System.out.println("通過 Optional 過濾的集合輸出:");
nameList.stream().forEach(System.out::println);
}
5、總結
本文首先,從所解決的問題開始,介紹了當前NPE處理所遇到的問題;然後,分類地介紹了Optional類中的方法並給出相應的示例;接着,從源碼層面對幾個常用的方法進行了對比;最後,列舉出了幾個常見的誤用形式和Best practice,結束了全文。
Optional類具有:可以顯式體現值可能為空的語義和隱藏可能存在空指針的不確定性的優點,但是同時也具有,適用範圍不是很廣(建議使用於返回值和NPE邏輯處理)以及使用時需要更多考量的缺點。
但是總體看來,Optional類是伴隨Java8函數式編程出現的一項新特性。為處理邏輯的實現提供了更多的選擇,未來期待更多的實踐和best practice出現,為Optional帶來更多出場的機會。
6、參考
[1]https://www.runoob.com/java/java8-optional-class.html來源:菜鳥教程
[2] https://blog.csdn.net/qq\_40741855/article/details/103251436來源:CSDN
[3] https://yanbin.blog/java8-optional-several-common-incorrect-usages/#more-8824來源:blog
[4] https://www.javaspecialists.eu/archive/Issue238-java.util.Optional---Short-Tutorial-by-Example.html來源:java specialists
[5] Java核心技術 卷II- Java8的流庫 - Optional類型
[6] https://www.zhihu.com/question/444199629/answer/1729637041來源:知乎
如有不當之處,望指正~