2015年4月13日

應付 NullPointerException 的 java.util.Optional

因為不可預期的 NullPointerException 而造成系統 crash,這應該是所有寫程式的人都曾經遇到的情況,從資料的狀態來看,null 確實是一種特殊的存在狀態,也必須區分開來,但偏偏 Java 無法直接使用 null 物件,因為會產生 NullPointerException,接下來就討論一下 null 物件跟應對的方法。

Boundary Test

在測試理論的 boundary test 中,我們要注意資料、狀態變化與一些功能的特殊狀況,測試並檢驗程式會不會出錯。在 Java 中最常見的會引發系統 crash 的錯誤,就是 NullPointerException,系統會在遇到資料為 null 的情況下,發生不可預期的錯誤,就因為程式設計師不認為這個資料會是 null,而一旦存取到了一個 null 的物件,就會造成 NullPointerException 而中斷整個程式,讓系統 offline。

null 是不是一個正確值

程式設計師在一般的 Java 物件時,通常就會注意到這個物件有沒有被初始化,那怎麼還會遇到 NullPointerException 呢?

最重要的原因就是 String 這個最常見的物件,再加上初始化 String 物件有 syntax sugar,在撰寫程式時,通常 String 變數會存放在客製物件中,也常常不會發覺這個字串變數,到底有沒有初始化,只知道在 Data Bean 裡面 寫上 get/set method 而已。

null 對一個物件來說,也是一種正常的狀態,而這個狀態,代表的是這個物件所儲存的數值,還沒有被初始化。

null 跟 空字串 是不同的

不管是在資料庫或是在 Java 裡面,null 跟 空字串 都是不同的,也有不同的意義,null 基本上是一個標記,不暫用到任何記憶體或實體的空間。

而空字串 "" 就不同了,因為空字串是個正常的字串,長度為 0 的字串,在 Java 的字串 instance 中,就等於已經產生了一個長度為 0 的字串物件 instance,實際上是有佔用到記憶體的。

java.util.Optional

Java 8 提供一個新的 class java.util.Optional,可以封裝一個物件,並提供方法做 null 的檢測。基本上,所有可能會 return null 物件的 method 都應該改為 return Optional

建立 Optional Object

  • Optional empty()
    產生 empty Optional

  • Optional of(T value)
    產生 T value 的 Optional object,但如果 value 是 null,就會丟出 NullPointerException

  • Optional ofNullable(T value)
    產生 T value 的 Optional object,但如果 value 是 null,就會回傳 empty Optional

使用範例

    Optional<String> empty  = Optional.empty();
    System.out.println(empty);

    Optional<String> str = Optional.of("custom string");
    System.out.println(str);

    String nullableString = null; 
    Optional<String> str2  = Optional.ofNullable(nullableString);
    System.out.println(str2);

ifPresent, isPresent

# 檢查此 Optional 物件是不是 null
public boolean isPresent()

# 這是用 Lambda 語法,當 Optional 為 empty 時,就不執行後面的 Consumer。
public void ifPresent(Consumer<? super T> consumer)

使用的範例如下

Optional<String> str = Optional.ofNullable(null);
str.ifPresent(value -> System.out.println("Optional contains " + value));

if (str.isPresent()) {
    String value = str.get();
    System.out.println("Optional contains " + value);
} else {
    System.out.println("Optional is empty.");
}

Optional<String> str2 = Optional.ofNullable("custom string");
str2.ifPresent(value -> System.out.println("Optional contains " + value));

執行結果,只會看到有2行輸出

Optional is empty.
Optional contains custom string

get, orElse, orElseGet, orElseThrow, ifElse

# 取得 Optional 裡面存放的物件,但如果 Optional 物件是 null,就 throw NoSuchElementException
public T get()

# 取得 Optional 裡面存放的物件,但如果 Optional 物件是 null,就 return 物件 other
public T orElse(T other)

# 取得 Optional 裡面存放的物件,但如果 Optional 物件是 null,就呼叫 other 這個 supplier 的 get 取得下一個物件
public T orElseGet(Supplier<? extends T> other)

# 取得 Optional 裡面存放的物件,但如果 Optional 物件是 null,就呼叫 exceptionSupplier 的 get 產生的 Throwable Exception,例如  IllegalStateException::new
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X extends Throwable

使用範例

String defaultString = "default string";
Optional<String> optionalString = Optional.empty();
String resultString = optionalString.orElse(defaultString);
System.out.println("resultString: " + resultString);

optionalString = Optional.ofNullable("custom string");
resultString = optionalString.orElse(defaultString);
System.out.println("resultString: " + resultString);

optionalString = Optional.ofNullable(null);
optionalString.orElseThrow(IllegalStateException::new);

執行結果

resultString: default string
resultString: custom string
Exception in thread "main" java.lang.IllegalStateException
    at Main$$Lambda$1/8460669.get(Unknown Source)
    at java.util.Optional.orElseThrow(Optional.java:290)
    at Main.main(Main.java:44)

filtering and mapping with Lambdas

# 如果 Optional 裡面的 value 存在,且符合 predicate 的條件限制,就回傳這個 value,否則就回傳 empty Optional
public Optional<T> filter(Predicate<? super T> predicate)

# 如果 Optional 裡面的 value 存在,就執行 mapper function,如果 value 為 null,就回傳 empty Optional
public <U> Optional<U> map(Function<? super T,? extends U> mapper)

使用範例

Optional<Integer> emptyNumber = Optional.empty();
emptyNumber.filter(x -> x == 250).ifPresent(
        x -> System.out.println(x + " is ok!"));

Optional<Integer> cardNumber = Optional.of(new Integer(300));
cardNumber.filter(x -> x == 200).ifPresent(
        x -> System.out.println(x + " is ok!"));

cardNumber = Optional.of(new Integer(200));
cardNumber.filter(x -> x == 200).ifPresent(
        x -> System.out.println(x + " is ok!"));

執行結果

200 is ok!

IntStream 的 max 與 min 回傳了 OptionalInt

在 IntStream 裡面,max 以及 min 的回傳資料都是 OptionalInt,這是在提取一堆 Int 裡面的最大值,而這個最大值,當然有可能會因為給予的 IntStream 本身就不存在,導致根本找不到最大值而得到不存在的結果,為了描述這個「合理」的不存在的結果,就套用了 Optional 封裝這個結果。

使用範例

OptionalInt maxOdd = IntStream.of(10, 20, 30).filter(n -> n % 2 == 1).max();
if (maxOdd.isPresent()) {
    int value = maxOdd.getAsInt();
    System.out.println("Maximum odd integer is " + value);
} else {
    System.out.println("Stream is empty.");
}

執行結果

Stream is empty.

真的會使用 Optional 嗎?

關於這個問題,我的看法是:

  • 如果 null 是邏輯上正確的狀態,那就使用 Optional。
  • 如果有個物件變數,但不確定會不會是 null,要避開產生 NullPointerException,就用 Optional.ofNullable(object).orElse(defaultValue); 確保在遇到 null 的時候,會有預設值
  • 如果不應該發生 null,那就直接把初始值放進去就好了,不需要再用 Optional 封裝起來。

或許一般的程式設計中,不需要考慮到這個問題,畢竟如果在 javadoc 裡面看到一堆 method,回傳的物件是 Optional,使用起來也會產生一些困擾,畢竟不是 CustomClass 的物件,沒辦法在收到回傳物件後,就直接使用該物件的 method。

在沒有 Optional 之前,我們也可以使用 if( object==null ) 的方式去檢查是不是 null,用 object==null ? defaultValue : realValue 的方式,為物件設定預設值。

但也不需要排斥或拒絕使用 Optional,他等於是用了一個物件的方式,包裝 null 的狀態下的處理過程,因此就把 Optional 當作是一個習慣的寫法就好了。

重要的是程式設計師要注意 null

不管語言或語法的問題,就程式的邏輯來看,在設計程式語言時,就必須明確地將 null 與 "" 區分開來,因為他們本質上就是不一樣的兩個概念。

最重要的是程式設計師對 null 的認知,寫程式的時候,必須隨時意識到這個變數會不會有 null 的狀況,必須運用程式語言既有的機制,對自己的程式做出負責任的處理。

換句話說,到底這個物件會不會有 null 的情況,必須明確地在程式中,以各種方式標示出來,甚至可以在規避 NullPointerException 的前提下,率先在 method 的 return value 中加上 null 的檢查,並在 null 時改回傳 default value。

也許一個 @NotNull 或是 @Nullable 的 annotation,更明確地告訴程式設計師要注意 null 會更有用,但就不繼續討論了。

Reference

java2s Optional

Java 8 Optional Example

Java Gossip: 使用 Optional 取代 null