[Learn] Java Exception 與 Optional:處理失敗與缺值
/ 11 min read
Updated:Table of Contents
Java method 不一定每次都能正常回傳資料
有時操作無法完成,有時只是查詢結果不存在,這兩種情況不應使用相同方式處理
輸入不合法或操作失敗 → Exception查詢結果本來就可能不存在 → Optional呼叫者要求資料一定存在 → Optional.orElseThrow這篇會整理 checked exception、unchecked exception、try-catch 與 custom exception,再說明 Optional 適合解決什麼問題
Exception 表示操作無法正常完成
Java exception hierarchy 可以先簡化成以下結構
Throwable├── Error└── Exception ├── RuntimeException └── 其他 checked exceptionError通常表示應用程式難以合理恢復的 JVM 或環境問題- checked exception 會由 compiler 要求 catch 或在 method 宣告
throws RuntimeException與其 subclass 屬於 unchecked exception,compiler 不強制處理
常見的 unchecked exception 包含
IllegalArgumentExceptionIllegalStateExceptionNullPointerExceptionIndexOutOfBoundsExceptionNoSuchElementException
常見的 checked exception 包含 IOException 與 SQLException
throw、throws、try 與 catch
throw 用來實際拋出一個 exception object
if (score < 0) { throw new IllegalArgumentException("score 不可小於 0");}throws 寫在 method declaration,表示這個 method 可能把 exception 交給呼叫者處理
import java.io.IOException;import java.nio.file.Files;import java.nio.file.Path;
public static String load(Path path) throws IOException { return Files.readString(path);}呼叫 checked exception method 時,可以選擇 catch
try { String content = load(Path.of("config.txt")); System.out.println(content);} catch (IOException exception) { System.err.println("讀取設定失敗:" + exception.getMessage());}也可以在目前 method 繼續宣告 throws IOException,把決定交給更上層
finally 不論是否發生 exception 都會執行,適合放必要的收尾動作
處理 file、stream 或 connection 等 AutoCloseable resource 時,優先使用 try-with-resources
import java.io.BufferedReader;
try (BufferedReader reader = Files.newBufferedReader(Path.of("config.txt"))) { System.out.println(reader.readLine());} catch (IOException exception) { System.err.println("讀取設定失敗:" + exception.getMessage());}離開 try block 時,Java 會自動呼叫 resource 的 close
建立自己的 Exception
當失敗具有明確的 domain 意義,可以建立專用 exception
public final class DuplicateStudentException extends RuntimeException { public DuplicateStudentException(String id) { super("學號已存在:" + id); }}呼叫端可以明確 catch 它
try { registry.register(student);} catch (DuplicateStudentException exception) { System.err.println("註冊失敗:" + exception.getMessage());}處理 exception 時可以先遵守幾個原則
- catch 能處理的具體 exception,不要習慣性 catch
Exception - 不要留下空白 catch block
- 訊息要提供失敗操作與重要識別資訊
- 包裝底層 exception 時保留原始 cause
- 不要用 exception 代替一般流程控制
如果目前這一層不知道如何恢復,通常比起記錄後假裝成功,繼續往上拋會更合理
Optional 表示值可能不存在
如果查詢學生可能找不到,可以回傳 Optional<Student>
public Optional<Student> findById(String id) { return Optional.ofNullable(studentsById.get(id));}回傳型別直接告訴呼叫者,這次查詢可能沒有結果
建立 Optional 的方式如下
Optional<String> name = Optional.of("Amy");Optional<String> maybeName = Optional.ofNullable(value);Optional<String> empty = Optional.empty();Optional.of(value)不接受 nullOptional.ofNullable(value)會把 null 轉成 emptyOptional.empty()明確建立沒有值的 Optional
不要讓 Optional 自己是 null
// 不要這樣做return null;
// 沒有結果時回傳return Optional.empty();使用 Optional 處理結果
只需要在有值時執行動作,可以使用 ifPresent
registry.findById("S001") .ifPresent(student -> System.out.println(student.getName()));需要轉換內容,可以使用 map
String name = registry.findById("S001") .map(student -> student.getName()) .orElse("查無學生");需要留下符合條件的值,可以使用 filter
Optional<Student> amy = registry.findById("S001") .filter(student -> student.getName().equals("Amy"));mapping function 本身就回傳 Optional 時,使用 flatMap 避免得到巢狀的 Optional<Optional<T>>
沒有值時的常見選擇如下
Student fallback = optional.orElse(defaultStudent);Student lazyFallback = optional.orElseGet(() -> loadDefaultStudent());Student required = optional.orElseThrow( () -> new NoSuchElementException("找不到學生"));orElse 的 argument 會先被計算,即使 Optional 有值也一樣
建立預設值的成本較高或有 side effect 時,使用 lazy 的 orElseGet
不建議把程式重新寫回手動檢查後直接呼叫 get
if (optional.isPresent()) { Student student = optional.get();}這種寫法不是永遠錯誤,但 map、ifPresent、orElseGet 或 orElseThrow 通常更能直接表達意圖
Optional 最適合用在 method return type,表示可能沒有單一結果
Collection 沒有元素時直接回傳 empty collection,通常不需要再包一層 Optional
List<Student> students = List.of();Optional 與 Exception 如何選
兩者不是互相替代的工具
查詢結果本來就可能不存在 → Optional
呼叫者要求資料一定存在,但實際找不到 → Exception
輸入不合法或系統無法完成操作 → Exception同一個 repository 或 registry 可以同時提供兩種 API
public Optional<Student> findById(String id) { return Optional.ofNullable(studentsById.get(id));}
public Student getRequiredById(String id) { return findById(id).orElseThrow( () -> new NoSuchElementException("找不到學生:" + id));}findById 讓呼叫者處理正常的沒有結果
getRequiredById 則表達找不到就是目前操作的失敗
完整範例:StudentRegistry
建立以下目錄結構
src/└── com/ └── example/ ├── app/ │ └── Main.java └── student/ ├── Student.java ├── StudentRegistry.java └── DuplicateStudentException.java建立 Student.java
package com.example.student;
public final class Student { private final String id; private final String name;
public Student(String id, String name) { if (id == null || id.isBlank()) { throw new IllegalArgumentException("id 不可為空"); }
if (name == null || name.isBlank()) { throw new IllegalArgumentException("name 不可為空"); }
this.id = id; this.name = name; }
public String getId() { return id; }
public String getName() { return name; }}沿用前面的 DuplicateStudentException.java
package com.example.student;
public final class DuplicateStudentException extends RuntimeException { public DuplicateStudentException(String id) { super("學號已存在:" + id); }}建立 StudentRegistry.java
package com.example.student;
import java.util.HashMap;import java.util.Map;import java.util.NoSuchElementException;import java.util.Optional;
public final class StudentRegistry { private final Map<String, Student> studentsById = new HashMap<>();
public void register(Student student) { if (studentsById.containsKey(student.getId())) { throw new DuplicateStudentException(student.getId()); }
studentsById.put(student.getId(), student); }
public Optional<Student> findById(String id) { return Optional.ofNullable(studentsById.get(id)); }
public Student getRequiredById(String id) { return findById(id).orElseThrow( () -> new NoSuchElementException("找不到學生:" + id)); }}最後建立 Main.java
package com.example.app;
import com.example.student.DuplicateStudentException;import com.example.student.Student;import com.example.student.StudentRegistry;
import java.util.NoSuchElementException;
public class Main { public static void main(String[] args) { StudentRegistry registry = new StudentRegistry();
registry.register(new Student("S001", "Amy")); registry.register(new Student("S002", "Ben"));
String foundName = registry.findById("S002") .map(student -> student.getName()) .orElse("查無學生");
String missingName = registry.findById("S999") .map(student -> student.getName()) .orElse("查無學生");
System.out.println("找到:" + foundName); System.out.println("查詢 S999:" + missingName);
try { registry.register(new Student("S001", "Alice")); } catch (DuplicateStudentException exception) { System.out.println("註冊失敗:" + exception.getMessage()); }
try { registry.getRequiredById("S999"); } catch (NoSuchElementException exception) { System.out.println("查詢失敗:" + exception.getMessage()); } }}編譯並執行
javac -d out \ src/com/example/student/*.java \ src/com/example/app/Main.java
java -cp out com.example.app.Main執行結果如下
找到:Ben查詢 S999:查無學生註冊失敗:學號已存在:S001查詢失敗:找不到學生:S999同一個 registry 提供兩種查詢語意
findById表示沒有結果是正常情況getRequiredById表示目前操作要求學生一定存在register表示重複學號違反註冊規則
常見錯誤:catch 後什麼都不做
try { performOperation();} catch (Exception exception) {}這會讓失敗消失,呼叫端可能誤以為操作成功
應該在能恢復的層級處理,否則保留 context 後繼續拋出
常見錯誤:catch 過度寬廣的 Exception
如果真正能處理的是 IOException,就不應順便攔截其他 RuntimeException
優先 catch 具體型別,也讓不屬於目前責任的失敗繼續往上傳遞
常見錯誤:對 Optional 直接呼叫 get
Student student = registry.findById("S999").get();Optional 為 empty 時會拋出 NoSuchElementException
改用能表達意圖的 orElse、orElseGet、orElseThrow 或 ifPresent
常見錯誤:回傳 null Optional
Optional 本身不應該是 null
沒有值時回傳 Optional.empty(),讓呼叫者只處理一種缺值表示方式
常見錯誤:用 Exception 表示正常的查無結果
如果查無資料是預期情況,讓呼叫者每次都 catch exception 會讓正常流程難以閱讀
此時回傳 Optional 通常更能表達 API 語意
結論
- checked exception 必須 catch 或宣告 throws,unchecked exception 不受 compiler 強制
- throw 用來拋出 exception object,throws 用來宣告 method 可能傳遞的 exception
- 能恢復時才 catch,否則應保留 context 並交給合適的上層處理
- try-with-resources 適合管理 AutoCloseable resource
- Optional 表示單一結果可能不存在,不應回傳 null Optional
- orElse、orElseGet 與 orElseThrow 分別表達預設值、延遲建立與缺值失敗
- 預期中的查無結果適合 Optional,違反規則或操作失敗適合 Exception
參考資料
- https://docs.oracle.com/javase/specs/jls/se26/html/jls-11.html
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Throwable.html
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Exception.html
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/RuntimeException.html
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Optional.html
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/NoSuchElementException.html
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/nio/file/Files.html