京東雲開發者|深入JDK中的Optional

京東雲發表於2022-11-07

概述:Optional最早是Google公司Guava中的概念,代表的是可選值。Optional類從Java8版本開始加入豪華套餐,主要為了解決程式中的NPE問題,從而使得更少的顯式判空,防止程式碼汙染,另一方面,也使得領域模型中所隱藏的知識,得以顯式體現在程式碼中。Optional類位於java.util包下,對鏈式程式設計風格有一定的支援。實際上,Optional更像是一個容器,其中存放的成員變數是一個T型別的value,可值可Null,使用的是Wrapper模式,對value操作進行了包裝與設計。本文將從Optional所解決的問題開始,逐層解剖,由淺入深,文中會出現Optioanl方法之間的對比,實踐,誤用情況分析,優缺點等。與大家一起,對這項Java8中的新特性,進行理解和深入。

1、解決的問題

臭名昭著的空指標異常,是每個程式設計師都會遇到的一種常見異常,任何訪問物件的方法與屬性的呼叫,都可能會出現NullPointException,如果要確保不觸發異常,我們通常需要進行物件的判空操作。

舉個例子,有一個人(Shopper)進超市購物,可能會用購物車(Trolley)也可能會用其它方式,購物車裡可能會有一袋栗子(Chestnut),也可能沒有。三者定義的程式碼如下:


PlainJavascriptJavaHTML/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)

PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public String result(Shopper shopper){
  return shopper.getTrolley().getChestnut().getName();
}

為了能避免出現空指標異常,通常的寫法會逐層判空(多層巢狀法),如下

PlainJavascriptJavaHTML/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,相應的程式碼有會變得很冗長),所以此時我們也可以用遇空則返回的衛語句進行改寫。

PlainJavascriptJavaHTML/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就提供了一些方法,實現了這樣的期望。

PlainJavascriptJavaHTML/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構建物件。

PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
private Optional() {}
private Optional(T value) {}

但是兩者都是私有方法,而實際上Optional的物件都是透過靜態工廠模式的方式構建,主要有以下三個函式

PlainJavascriptJavaHTML/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

PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Chestnut chestnut = new Chestnut();
Optional<Chestnut> opChest = Optional.of(chestnut);

建立一個空的Optional物件

PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Optional<Chestnut> opChest = Optional.empty();

建立一個可空的Optional物件

PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Chestnut chestnut = null;
Optional<Chestnut> opChest = Optional.ofNullable(chestnut);

2)正常使用

正常使用的方法可以被大致分為三種型別,判斷類操作類取值類

PlainJavascriptJavaHTML/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其他的方法處理會更為常見。如下程式碼將會列印出沒有栗子的悲慘事實。

PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Optional<Chestnut> opChest = Optional.empty();
if (!opChest.isPresent()){
  System.out.println("容器裡沒有栗子");
}

ifPresent()方法是一個操作類的方法,他的引數是一段目標型別為Consumer的函式,當value不為空時,自動執行consumer中的accept()方法(傳入時實現),為空則不執行任何操作。比如下面這段程式碼,我們傳入了一段輸出value的lamda表示式,列印出了“遷西板栗”。

PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Optional<Chestnut> opChest = Optional.ofNullable(new Chestnut("遷西板栗"));
opChest.ifPresent(c -> System.out.println(c.getName()));

get()方法原始碼如下,可以看出,get的作用是直接返回容器中的value。但如此粗暴的方法,使用前如果不判空,在value為空時,便會毫不留情地丟擲一個異常。

PlainJavascriptJavaHTML/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()方法會丟擲一個自定的異常。更具體的差別會在後面的方法對比中描述。

如下面這段程式碼,展示了在沒有栗子的時候,如何吐出“信陽板栗”、“鎮安板栗”,以及丟擲“抹油栗子”的警告。

PlainJavascriptJavaHTML/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)進階使用

PlainJavascriptJavaHTML/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一致,所以最後列印出來的是“寬城板栗”。

PlainJavascriptJavaHTML/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了一個板栗取名“邢臺板栗”,並列印出來,兩者輸出一致,處理形式上有差異,這個在後面的方法對比中會再次說到。


PlainJavascriptJavaHTML/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新增

PlainJavascriptJavaHTML/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語義的邏輯。如下面程式碼,會列印出“木有栗子”的提示。

PlainJavascriptJavaHTML/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返回,其原始碼中包裝的語句如下:

PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Optional<T> r = (Optional<T>) supplier.get();
return Objects.requireNonNull(r);

stream()方法則不用多說,是一個提供給流式程式設計使用的方法,功能上是一個介面卡,將Optional轉換成Stream:沒有值返回一個空的stream,或者包含一個Optional的stream。其原始碼如下:

PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
if (!isPresent()) {
  return Stream.empty();
} else {
  return Stream.of(value);
}


3、方法對比和總結

Optional封裝的方法較多,選擇一個合適的方法的前提是要了解各自適用的場景和異同

1)建立方法的對比

由於構造器為私有方法,建立物件只能透過靜態工廠的方式建立。of()、ofNullable()和empty()方法是三個靜態方法。先上原始碼:

PlainJavascriptJavaHTML/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()非空校驗,就會丟擲一個異常,程式碼如下:

PlainJavascriptJavaHTML/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,但是為空時的處理各不相同。先上原始碼:

PlainJavascriptJavaHTML/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()方法最直觀的差異是形參的不同,看下面一段程式碼:

PlainJavascriptJavaHTML/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()方法,結果居然還出現了一行“燕山板栗最好吃”的輸出。

PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
第一次輸出:
燕山板栗最好吃----orELse
燕山板栗最好吃----orElseGet
第二次輸出:
燕山板栗最好吃----orELse

其原因是orElseGet()的引數是Supplier目標型別的函式,簡單來說,Suppiler介面類似Spring的懶載入,宣告之後並不會佔用記憶體,只有執行了get()方法之後,才會呼叫構造方法建立出物件,而orElse()是快載入,即使沒有呼叫,也會實際的執行。

這個特性在一些簡單的方法上差距不大,但是當方法是一些執行密集型的呼叫時,比如遠端呼叫,計算類或者查詢類方法時,會損耗一定的效能。

orElseThrow()方法與orElseGet()方法的引數都是函式型別的,這意味著這兩種方法都是懶載入,但針對於必須要使用異常控制流程的場景,orElseThrow()會更加合適,因為可以控制異常型別,使得相比NPE會有更豐富的語義。

3)其他方法的對比

a、map與filterMap

先上原始碼:

PlainJavascriptJavaHTML/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()的改進方法出現的。先看原始碼:

PlainJavascriptJavaHTML/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系列方法的“小缺點”出現的,先看原始碼:

PlainJavascriptJavaHTML/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型別變數是預設不為空的,所以在取方法執行的時候才可以肆無忌憚"點"出去,如果在初始化的時候出現:

PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Optional<Chestnut> chest = null;

並且不及時為chest賦值,則還是容易出現NPE,正確的初始化方式應該是:

PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Optional<Chestnut> chest = Optional.empty();

b、簡單判空

第二種比較常見的誤用形式應該是使用isPresent()做簡單判空。原本的程式碼如下:

PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public String getName(Chestnut chestnut){
	if(chestnut == null){
		return "栗子不存在";
	}else return chestnut.name();
}

程式碼中,透過檢查chestnut == null來處理為空時的情況,簡單使用isPresent()方法判空的程式碼如下:


PlainJavascriptJavaHTML/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.OptionalDoublejava.util.OptionalIntjava.util.OptionalLong or com.google.common.base.Optional as 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.Optional is also problematic if the class needs to be Serializable, which java.util.Optional is not.

大意是不建議如此使用Optional。第一,不建議使用Optional作為欄位或引數,其設計是為庫方法返回型別提供一種有限的機制,而這種機制可以清晰的表示“沒有結果”的語義;第二,Optional沒有實現Serilazable,是不可被序列化的。

這種誤用方法比較明顯,復現和避免也比較簡單。但筆者還想就這兩個建議的原因做進一步的探究,所以查閱了一些資料,大體的原因如下:

第一個原因,為什麼不適合做屬性欄位和方法引數?直白的說,就是麻煩。為了引入Optional,卻需要加入多段樣板程式碼,比如判空操作。使得在不合適的位置使用Optional不僅沒有給我們帶來便利,反而約束了寫程式碼的邏輯。

寫以下域模型程式碼

PlainJavascriptJavaHTML/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包裝一層後再傳入,使得程式碼顯得累贅了,如下:

PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
chest.setMidName(Optional.empty());
chest.setMidName(Optional.of("阿慄"));

在屬性不為Optional的時候,如果給屬性賦值,需要使用“消費”操作,比如orElse(),取出值再賦給屬性,相比直接傳入String型別的值作為欄位和形參可以減少這些步驟,後者反而更加合適。

第二個原因,為什麼沒有實現序列化?相關可以參見Java Lamda的專家組討論

京東雲開發者|深入JDK中的Optional

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.Optional and 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 equalshashCode, and toString which 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 x and y that 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、示例一

PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Chestnut chestnut = new Chestnut("錐慄板栗");
if(chestnut != null){
  String chestName = chestnut.getName();
  if(chestName != null){
    return chestName.concat("好好吃!");
  }
}else{
  return null;
}

可以簡化成:

PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Chestnut chestnut = new Chestnut("錐慄板栗");
return Optional.ofNullable(chestnut)
              .map(Chestnut::getName)
              .map(name->name.concat("好好吃!"))
              .orElse(null);

b、示例二

PlainJavascriptJavaHTML/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://mp.weixin.qq.com/s/q_WmD3oMvgPhakiPLAq-CQ 來源:wechat

[2] https://www.runoob.com/java/java8-optional-class.html 來源:菜鳥教程

[3] https://blog.csdn.net/qq_40741855/article/details/103251436 來源:CSDN

[4] https://yanbin.blog/java8-optional-several-common-incorrect-usages/#more-8824 來源:blog

[5] https://www.javaspecialists.eu/archive/Issue238-java.util.Optional---Short-Tutorial-by-Example.html 來源:java specialists

[6] Java核心技術 卷II - Java8的流庫 - Optional型別

[7] https://www.zhihu.com/question/444199629/answer/1729637041 來源:知乎


如有不當之處,望指正~


作者:歷子謙


相關文章