Java 8 新加入了 Optional 類別,能省去繁瑣的 null check 流程,豐富的 API 也讓程式邏輯看起來更簡潔、易讀。但我卻看到了不少錯誤的用法,反而讓 Optional 顯得多此一舉。本篇探討這些錯誤的用法,以及如何正確使用。

isPresent() and get()

假設有一個 studentService 可利用 id 查詢學生資料,我們為了避免 return null 而後續可能導致 NPE,我們就必需在 studentService.readById 回傳結果時先做 null check,因此傳統寫法會像這樣:

public Student readById(String id) {
    Student student = studentService.readById(id);
    if (student != null) {
        return student;
    } else {
        throw new NotFoundException(id); 
    }
}

若改成 Optional 寫法,並將 studentService.readById 改為回傳 Optional<Student> 後,有些人可能會寫成這樣 :

public Student readById(String id) {
    Optional<Student> student = studentService.readById(id);
    if (student.isPresent()) {
        return student.get();
    } else {
        throw new NotFoundException(id); 
    }
}

很不幸的是,這應該是最常見的錯誤用法了,我們不難發現上面的 isPresent(), get() 和傳統寫法本質上是一樣的,且增加了不必要的複雜度,可謂多此一舉。

正確使用 Optional 方式如下:

public Student readById(String id) {
    return studentService.readById(id).orElseThrow(() -> new NotFoundException(id));
}

orElseThrow 會判斷 Optional 的內容,若有值時則直接回傳 Student;若沒有,則拋出例外。不難看出 Optional 是與 Java 8 functional programming 寫法相輔相成的,所以使用 Optional 時應搭配如 filter(), map(), orElseThrow() 等的 functional programming 風格的寫法會比較適合。

一定有值,卻依然使用 Optional

Optional 設計的意義就是用來表示 method 的回傳值可能會是空的。但在某些一定會有回傳值情況下,開發者卻依然使用 Optional,這就造成了過度包裝與多此一舉。承上學生系統的例子,假設我們要查詢全體學生中的第一名:

public Optional<Student> readTopScoreStudent() {
    // ...
}

正常來說,這個系統並不會沒有學生資料(否則一切都是空談),因此這個 method 肯定會有回傳值,不需使用 Optional。通常需要透過 code review 才能發現類似的問題。

作為參數

有些人會將 Optional 作為參數,意圖表示這個參數可能是非必要的:

public void setName(Optional<String> name) {
    if (name.isPresent()) {
        this.name = name.get();
    } else {
        this.name = "無名氏";
    }
}

但這是個不好的寫法,因為這裡的 Optional<String> 參數有三種可能的值:

  • 有內容值的 Optional
  • Optional.empty()
  • null

Optional 也有可能是個 null,當然有機會引發 NPE,讓人更摸不著頭緒,因此請不要使用 Optional 作為參數。此外,這樣的寫法會讓 caller 很麻煩,因為他們必需將參數多包一層 Optional,變得不容易使用:

setName(Optional.of("Jason"));
setName(Optional.empty());

因此,比較好的設計是透過 overloading,讓參數有值或沒有值的意圖與結果更加明確:

public void setName() {
    this.name = "無名氏";
}

public void setName(String name) {
    this.name = name;
}

另外,有此一說,Optional 若作為 Spring controller 的參數,則更能表達該參數是非必要的,例如:

@RequestMapping (value = "/submit/id/{id}", method = RequestMethod.GET, produces="text/xml")
public String showLoginWindow(@PathVariable("id") String id,
                              @RequestParam("username") Optional<String> username,
                              @RequestParam("password") Optional<String> password) { ... }

在 Spring 4.1.1 後已經可以妥善處理這裡的 Optional,它將不會是 null,再加上它是 controller,所以也不會難以被呼叫,因此有些人覺得這種作法比較好,這就見仁見智了。

作為 class field

public class Student {

    private Optional<String> name;
    // ...
}

因為 Optional 是設計用來 method 的回傳型態,因此它並沒有實作序列化 Serializable 介面,在特定狀況下需要物件序列化時將會出現問題。

Collection and Optional

因為 Optional 本身就是一個容器,如果內容又是另一個容器,例如回傳 Optional<List<Student>>,這不僅比較複雜以外,在語意上還代表著三種可能的回傳值:

  • 一個有內容的 List
  • 一個空的 List
  • Optional.empty()

這樣容易造成程式複雜與混淆,比較好的方式是:如果真的沒有回傳值,那就回傳一個空的容器就好了:

Map and Optional

不要將 Optional 放入 Map,例如 Map<String, Optional<Student>>,原因和上述類似,在呼叫 map.get(key) 的回傳值會是:

  • Optional (可能有 Student 與可能沒有 Student)
  • null

像這種錯誤用法都會提高不必要的複雜性。

References

更多你可能會感興趣的文章

DesignJava

view: