Java Optional API

一位智者說過,沒有處理過空指針異常就不算一個真正的 Java 程序員。這當然是開玩笑,但是空指針異常確實是很多程序出錯的源頭。
於是,在 Java 8 引入了 java.util.Optional,Optional 用來代表一種 可能有可能沒有 的數據,可以用來緩解空指針異常的問題。

簡單地說,Optional 用來避免這種代碼:

String version = "UNKNOWN";
if(computer != null){
  Soundcard soundcard = computer.getSoundcard();
  if(soundcard != null){
    USB usb = soundcard.getUSB();
    if(usb != null){
      version = usb.getVersion();
    }
  }
}

如果用 Optional 表示呢?大概是這樣:

String version = computer.flatMap(Computer::getSoundcard)
                         .flatMap(Soundcard::getUSB)
                         .map(USB::getVersion)
                         .orElse("UNKNOWN");

實際上,Optional 即函數式編程中的 Maybe。

以下示例在 OptionalTest.java 中。

創建

創建 Optional 有三種方式,分別是 empty、 of 和 ofNullable。

empty

empty 用來創建一個空的 Optional

@Test
public void create_optional_with_empty() {
    Optional<String> empty = Optional.empty();
    assertFalse(empty.isPresent());
}

of

of 用來創建一個非空的 Optional:

@Test
public void create_optional_with_of() {
    Optional<String> java = Optional.of("Java");
    assertTrue(java.isPresent());
}

但是參數不能爲 null,否則會拋空指針異常:

@Test(expected = NullPointerException.class)
public void create_optional_with_of_with_null() {
    Optional.of(null);
}

ofNullable

ofNullable 用來創建一個可能爲空的 Optional:

@Test
public void create_optional_with_ofNullable() {
    Optional<String> java = Optional.ofNullable("Java");
    assertTrue(java.isPresent());

    Optional<Object> o = Optional.ofNullable(null);
    assertFalse(o.isPresent());
}

檢測值是否存在

可以使用 isPresentisEmpty 判斷 Optional 的值是否爲空。

isPresent

如果 Optional 中值非 null,則返回 true,否則返回 false。

@Test
public void check_optional_with_isPresent() {
    Optional<String> java = Optional.ofNullable("java");
    Optional<Object> aNull = Optional.ofNullable(null);

    assertTrue(java.isPresent());
    assertFalse(aNull.isPresent());
}

isEmpty

Java 11 開始可以使用 isEmpty

isEmptyisPresent 相反,如果爲 null 返回 true。

@Test
public void check_optional_with_isEmpty() {
    Optional<String> java = Optional.ofNullable("java");
    Optional<Object> aNull = Optional.ofNullable(null);

    assertFalse(java.isEmpty());
    assertTrue(aNull.isEmpty());
}

條件動作

關於條件的動作有 ifPresentorElseorElseGetorElseThroworifPresentOrElse,它們執行與否取決於 Optional 的值是否爲 null。

爲了避免空指針異常,我們會經常寫下面的代碼:

if (name != null){
    System.out.println(name.length);
}

Optional 使用一種函數式的方式來替代上面的寫法。

ifPresent

ifPresent 接受一個 Consumer,在 Optional 值非 null 時調用,並接受 Optional 的值。

@Test
public void condition_action_ifPresent() {
    Optional<String> java = Optional.ofNullable("java");
    java.ifPresent((value) -> System.out.println("ifPresent accept " + value));

    Optional<Object> aNull = Optional.ofNullable(null);
    aNull.ifPresent(value -> System.out.println("this will never execute"));
}

orElse

orElse 在 Optional 值爲 null 時觸發,它接受一個參數,作爲 Optional 的默認值。

@Test
public void condition_action_orElse() {
    assertTrue(Optional.ofNullable("java").orElse("javascript").equals("java"));
    assertTrue(Optional.ofNullable(null).orElse("java").equals("java"));
}

orElseGet

orElseGet 與 orElse 類似,但 orElseGet 接受的是一個 Supplier,Supplier 返回的值作爲 Optional 的默認值。

@Test
public void condition_action_orElseGet() {
    assertTrue(Optional.ofNullable("java").orElseGet(() -> "javascript").equals("java"));
    assertTrue(Optional.ofNullable(null).orElseGet(() -> "java").equals("java"));
}

orElse 和 orElseGet 的區別

orElseorElseGet 的函數簽名是不一樣的,但如果想使用同樣的函數的返回值來作爲 Optional 的默認值,我們很可能會這麼幹:

public String getDefaultName() {
    System.out.println("You got a default name");
    return "default";
}
    
@Test
public void difference_between_orElse_and_orElseGet() {
    Optional<String> java = Optional.of("java");

    System.out.println("orElse:");
    assertEquals("java", java.orElse(getDefaultName()));
    System.out.println("orElseGet:");
    assertEquals("java", java.orElseGet(this::getDefaultName));
}

若 java 是 null,則 orElse 和 orElseGet 沒有什麼不同,getDefaultName 方法都會執行並將返回值作爲 Optional 的默認值。

當在上面的例子中,java 非 null,這時 orElse 的 getDefaultName 還是會執行,但 orElseGet 不會。輸出:

orElse:
You got a default name
orElseGet:

當 getDefaultName 中有副作用或耗時操作時需要注意。

orElseThrow

orElseThrow 與 orElse 一樣也在當 Optional 值爲 null 時觸發,但與之不同的是會拋出指定的異常:

@Test(expected = IllegalArgumentException.class)
public void condition_action_orElseThrow() {
    Optional.ofNullable(null).orElseThrow(IllegalArgumentException::new);
}

or

or 是 Java 9 中新增方法。與 orElseGet 很相似,or 也接受一個 Supplier,但 or 返回的是一個新的 Optional。

@Test
public void condition_or_optional() {
    Optional<String> java = Optional.of("java")
                                    .or(() -> Optional.of("javascript"));
    Optional<Object> java1 = Optional.empty()
                                     .or(() -> Optional.of("java"));
    assertEquals("java", java.get());
    assertEquals("java", java1.get());
}

ifPresentOrElse

ifPresentOrElse 是 Java 9 中新增的方法。ifPresent 就如同命令式編程中的 if-else,它接受兩個參數,第一個爲 Consumer,在 Optional 有值時調用,第二個爲 Runnable,在無值時調用:

@Test
public void condition_ifPresentOrElse() {
    // value is java
    Optional.of("java")
            .ifPresentOrElse(value -> System.out.println("value is " + value), () -> System.out.println("ooops"));

    // ooops
    Optional.empty()
            .ifPresentOrElse(value -> System.out.println("value is " + value), () -> System.out.println("ooops"));
}

獲取值

Optional 提供了一個 get 方法獲取值,但 get 方法只能在 Optional 有值時使用,否則會拋出 NoSuchElementException 異常:

@Test
public void get_optional_with_of() {
    Optional<String> java = Optional.of("Java");
    assertEquals("java", java.get());
}

@Test(expected = NoSuchElementException.class)
public void get_optional_with_of_with_null() {
    Optional.empty().get();
}

驗證值

filter 方法用來驗證 Optional 的值是否符合條件,它接受一個 Predicate 作爲參數。如果 Optional 的值爲 null 或 Predicate 判斷不通過,則返回 empty;否則返回該 Optional。

@Test
public void test_optional_by_filter() {
    Integer nullYear = null;
    Optional<Integer> integer = Optional.ofNullable(nullYear)
                                        .filter(value -> value == 2018);
    assertEquals(Optional.empty(), integer);

    Integer year = 2019;
    Optional<Integer> integer1 = Optional.ofNullable(year)
                                         .filter(value -> value == 2018);
    assertEquals(Optional.empty(), integer1);

    Optional<Integer> integer2 = Optional.ofNullable(year)
                                         .filter(value -> value == 2019);
    assertEquals("Optional[2019]", integer2.toString());
}

filter 相對傳統 if 而言省去了很多樣板代碼,如:

public boolean priceIsInRange1(Modem modem) {
    boolean isInRange = false;
 
    if (modem != null && modem.getPrice() != null
      && (modem.getPrice() >= 10
        && modem.getPrice() <= 15)) {
 
        isInRange = true;
    }
    return isInRange;
}

使用 Optional 實現同樣的方法:

public boolean priceIsInRange2(Modem modem2) {
     return Optional.ofNullable(modem2)
       .map(Modem::getPrice)
       .filter(p -> p >= 10)
       .filter(p -> p <= 15)
       .isPresent();
}

處理值

處理值的方式有 map 和 flatMap。

map

使用 map 可以對 Optional 中的值進行處理並返回。

@Test
public void map_optional() {
    Optional<String> java = Optional.of("java");
    String result = java.map(String::toUpperCase).orElse("");
    assertEquals("JAVA", result);
}

flatMap

flatMap 與 map 的區別在於 map 處理值後會包裝返回值,而 flatMap 不包裝。

public class Person {
    private String name;

    public Person(String name) {
        this.name = name;
    }

    public Optional<String> getName() {
        return Optional.ofNullable(name);
    }
}

@Test
public void flatMap_optional() {
    Person person = new Person("john");
    Optional<Person> personOptional = Optional.of(person);

    String byMap = personOptional.map(Person::getName)
                                 // 需要手動打開包裝
                                 .orElse(Optional.empty())
                                 .orElse("");

    String byFlatMap = personOptional.flatMap(Person::getName)
                                     .orElse("");

    assertEquals("john", byMap);
    assertEquals("john", byFlatMap);
}

流操作

在 Java 9 中,新增了 stream 方法,可以對 Optional 創建 stream,然後可以使用 stream 上的所有方法。

如果 Optional 爲 empty,則創建一個 empty 的 stream。

@Test
public void treat_optional_as_stream() {
    List<String> collect = Optional.of("java")
                                   .stream()
                                   .map(value -> value.concat("script"))
                                   .collect(Collectors.toList());

    assertArrayEquals(new String[]{"javascript"}, collect.toArray());


    // empty optional
    Optional<String> value = Optional.empty();
    List<String> emptyStream = value.stream()
                                    .map(String::toUpperCase)
                                    .collect(Collectors.toList());

    assertEquals(0, emptyStream.size());
}

所以使用 stram 也可以篩出非 null 的 Optional 的值:

@Test
public void filter_empty_by_stream() {
    List<Optional<String>> languages = List.of(Optional.of("java"), Optional.empty(), Optional.empty(), Optional.of("javascript"));
    List<String> collect = languages.stream()
                                    .flatMap(Optional::stream)
                                    .collect(Collectors.toList());

    assertArrayEquals(new String[]{"java", "javascript"}, collect.toArray());
}

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章