整理 Effective Java 書中 Item 55: Use Optional judiciously 心得筆記

主旨

在 Java 8 以前,當方法沒辦法回傳有效值時,我們只有兩個選擇:拋出例外回傳 null。但這兩者都有明顯缺點。Java 8 引入的 Optional<T> 提供了更安全、更具表達力的選項,但這並不表示它應該無腦使用。

本篇會釐清什麼情況該用 Optional、什麼情況不該用,並提供實務建議。

點出問題:null 與例外都不完美

  • null 的問題:
    • 呼叫端需額外寫 null 判斷,容易遺漏
    • 容易導致晚發性 NullPointerException
  • 拋出例外的問題:
    • 應只用於真正異常的情況(Item 69)
    • 創建例外物件會記錄堆疊,成本高
    • 呼叫端需要寫 try-catch,降低可讀性

Optional 的出現

Optional<T> 是個容器,要嘛包著一個非 null 的 T,要嘛是空的。

Optional<String> maybeName = Optional.of("Tom");     // 有值
Optional<String> emptyName = Optional.empty();       // 無值

用法很直觀,也提升了 API 的表達力:

String lastWord = max(words).orElse("No words...");
Toy toy = max(toys).orElseThrow(TemperTantrumException::new);

相較於 null 或例外,Optional<T> 更能強迫呼叫者面對「可能沒有值」的狀況。

範例:取最大值

// ✅ 使用 Optional 作為回傳值
public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) {
    if (c.isEmpty()) return Optional.empty();
    E result = null;
    for (E e : c) {
        if (result == null || e.compareTo(result) > 0)
            result = Objects.requireNonNull(e);
    }
    return Optional.of(result);
}

Java 8 stream 版本更簡潔:

return c.stream().max(Comparator.naturalOrder());

如何處理 Optional?

  • 有預設值:.orElse(defaultValue)
  • 有預設邏輯:.orElseGet(() -> computeDefault())
  • 拋出自訂例外:.orElseThrow(MyException::new)
  • 確定有值:.get()(⚠️小心使用)
  • 函數式轉換:.map(...).flatMap(...)
  • 判斷是否有值:.isPresent()(通常可用 map/orElse 替代)

對照用法:

// 傳統寫法
String name = obj.getName();
System.out.println(name != null ? name : "N/A");

// Optional 寫法
System.out.println(obj.getNameOptional().orElse("N/A"));

不該用 Optional 的情境

  • ❌ 不要回傳 Optional<List<T>>Optional<Stream<T>>
    • 用空集合即可(詳見 Item 54)
  • ❌ 不要拿 Optional 當 Map 的 value
    • 會導致「key 不存在」與「key 存在但值為空」兩種語義衝突
  • ❌ 不要在效能敏感區域用 Optional<Integer> 等 boxing 型別
    • 改用 OptionalInt, OptionalLong, OptionalDouble
  • ⚠️ 小心用 Optional 作為物件欄位
    • 若欄位太多為 optional,可能代表應該要拆成子類別
    • 但在需要表達「可能沒有值」的 primitive 欄位上可以考慮使用

Java 9 新增功能

  • Optional::stream():搭配 flatMap 處理 Stream<Optional<T>>
Stream<Optional<T>> stream = ...;
Stream<T> result = stream.flatMap(Optional::stream);
  • ifPresentOrElse()or() 提供更多分支處理方式

小結

✅ 該使用 Optional 的情境:

  • 方法 可能沒有值可以回傳
  • 呼叫者 應該要處理沒有值的情況
  • 不適合用 null,也不夠嚴重到拋出例外

⚠️ 不建議用 Optional 的情境:

  • 容器型別(List、Map、Stream) → 回傳空集合
  • 效能敏感程式區塊 → 避免 Boxing/Unboxing 的成本
  • 非回傳值用途(欄位、集合內元素、Map 值)