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());
}
複製程式碼
檢測值是否存在
可以使用 isPresent
和 isEmpty
判斷 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
。
isEmpty
與 isPresent
相反,如果為 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());
}
複製程式碼
條件動作
關於條件的動作有 ifPresent
、orElse
、orElseGet
、orElseThrow
、or
、ifPresentOrElse
,它們執行與否取決於 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 的區別
orElse
和 orElseGet
的函式簽名是不一樣的,但如果想使用同樣的函式的返回值來作為 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 傳入的 mapper 要求返回值是 Optional 的子類,使用者可以自定義包裝類。
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.of("javascript"));
List<String> collect = languages.stream()
.flatMap(Optional::stream)
.collect(Collectors.toList());
assertArrayEquals(new String[]{"java","javascript"},collect.toArray());
}
複製程式碼