1. 問題:非同步程式碼中的例外
在一般(同步)程式碼中很簡單:如果方法中發生例外,它會沿呼叫堆疊「拋」上去,我們可以用 try-catch 來捕捉。例如:
try {
int x = 1 / 0;
} catch (ArithmeticException ex) {
System.out.println("除以零!");
}
在非同步程式碼中情況更複雜。當我們透過 CompletableFuture.supplyAsync 啟動任務時,它會在另一個執行緒中執行。如果那裡發生例外,它不會拋回主執行緒!相反地,它會被「封裝」在 CompletableFuture 物件中,之後如果你呼叫 get() 或 join(),就會以 ExecutionException 的形式收到該例外。
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
// 糟了,這裡出錯了!
return 1 / 0;
});
try {
Integer result = future.get(); // 這裡會拋出例外!
} catch (Exception ex) {
System.out.println("發生錯誤: " + ex.getMessage());
}
但如果你不呼叫 get()(順帶一提,這本身就不太「非同步」),而是透過 thenApply 等方法來串接,錯誤可能會「悄悄消失」。因此,在非同步程式設計中,務必要在 CompletableFuture 的鏈中捕捉並處理錯誤。
2. exceptionally 方法:處理錯誤並回傳值
exceptionally 方法允許你在鏈的前一個或多個階段出現例外時加以捕捉、處理,並回傳替代值。這就像 catch,但用於非同步的資料流。
簽名:
CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn)
使用範例
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println("正在執行危險的計算...");
if (Math.random() > 0.5) {
throw new RuntimeException("出現問題了!");
}
return 42;
});
future = future.exceptionally(ex -> {
System.out.println("發生錯誤: " + ex.getMessage());
return 0; // 回傳「安全」值
});
thenAccept 範例
future.thenAccept(result -> System.out.println("結果: " + result));
輸出(可能的結果):
正在執行危險的計算...
發生錯誤: 出現問題了!
結果: 0
正在執行危險的計算...
結果: 42
重要! exceptionally 只有在它之前的鏈條中出現未處理的例外時才會觸發。若一切順利,它會直接將結果傳遞到下一步。
3. handle 方法:同時處理結果與錯誤的通用處理器
有時我們需要同時處理結果與錯誤。例如,一切正常——就回傳結果;如果出錯——回傳備用方案或記錄錯誤。
簽名:
CompletableFuture<U> handle(BiFunction<? super T, Throwable, ? extends U> fn)
- 第一個參數 — 結果(若發生錯誤則為 null),
- 第二個 — 例外(若一切正常則為 null)。
使用範例
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
if (Math.random() > 0.5) throw new RuntimeException("隨機錯誤!");
return 100;
});
CompletableFuture<Integer> safeFuture = future.handle((result, ex) -> {
if (ex != null) {
System.out.println("偵測到錯誤: " + ex.getMessage());
return -1;
}
return result;
});
safeFuture.thenAccept(r -> System.out.println("最終結果: " + r));
輸出:
偵測到錯誤: 隨機錯誤!
最終結果: -1
最終結果: 100
當你希望不論任務是成功或失敗都要採取動作時,適合使用 handle。它是永遠會被呼叫的通用結果處理器,並接收兩個參數:結果(若一切正常)與例外(若發生錯誤)。
此方法非常適合集中記錄錯誤、在不終止鏈的情況下回傳預設值,或是優雅地結束非同步情境。
範例:
CompletableFuture<Integer> future = CompletableFuture
.supplyAsync(() -> 10 / 0) // 這裡會發生錯誤
.handle((result, ex) -> {
if (ex != null) {
System.out.println("錯誤: " + ex.getMessage());
return 0; // 預設值
}
return result;
});
System.out.println(future.join()); // 會輸出 0
不同於 exceptionally 僅在發生錯誤時才會反應,handle 會總是觸發,讓你在同一處理器內處理兩種結果,並維持整條鏈的順暢。
4. whenComplete 方法:完成後的副作用動作
有時我們不需要改變結果,只想在任務完成後執行某個動作——例如記錄任務已完成,無論是成功或失敗。
簽名:
CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throwable> action)
- 第一個參數 — 結果(若發生錯誤則為 null),
- 第二個 — 例外(若成功則為 null)。
使用範例
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
if (Math.random() > 0.5) throw new RuntimeException("錯誤!");
return 10;
});
future.whenComplete((result, ex) -> {
if (ex != null) {
System.out.println("執行時發生錯誤: " + ex.getMessage());
} else {
System.out.println("成功完成,結果: " + result);
}
});
重要差異:
whenComplete 不會改變結果或例外,只是執行動作。如果在 whenComplete 中發生新的例外,它會被「附加」到既有的例外上。
範例:只記錄,不干預
future
.whenComplete((res, ex) -> {
System.out.println("任務已完成。有錯誤嗎? " + (ex != null));
})
.thenAccept(r -> System.out.println("提供給使用者的結果: " + r));
5. 特性與實作細節
Best practices:如何正確在 CompletableFuture 中處理錯誤
- 務必在非同步任務鏈中加入錯誤處理(exceptionally、handle 或 whenComplete)。否則,錯誤可能不被注意,應用程式會出現不可預期的行為。
- 不要在主執行緒中,在沒有 try-catch 的情況下呼叫 get() 或 join()——這會把非同步程式碼變回同步,且可能導致阻塞。
- 若需要在錯誤時回傳「備用」值——使用 exceptionally 或 handle。
- 若是為了副作用(記錄日誌、通知使用者)——使用 whenComplete。
- 在鏈中可以組合:例如先用 exceptionally 處理錯誤,再用 whenComplete 記錄,接著繼續處理結果。
- 請記得,若錯誤未被處理,它會「流向」下一個呼叫 get()/join() 的地方,並可能導致應用程式當機。
方法順序
- 如果你使用 exceptionally,它只會攔截在它之前出現的錯誤。
- 如果在 exceptionally 之後鏈中再次發生錯誤(例如在 thenApply 中),需要另外處理。
- handle 是通用的——無論是否發生錯誤都會觸發。
方法的組合
CompletableFuture.supplyAsync(() -> {
// ...
})
.handle((result, ex) -> {
if (ex != null) return "錯誤: " + ex.getMessage();
return result;
})
.whenComplete((res, ex) -> {
System.out.println("任務已結束,結果: " + res);
});
如果沒有處理錯誤會怎樣?
如果例外未被處理而你呼叫了 get() 或 join(),它會以 ExecutionException(或 CompletionException)的形式被拋出,應用程式可能因此失敗。
6. 在 CompletableFuture 中處理錯誤的常見錯誤
錯誤 №1:未加入任何錯誤處理。 如果既沒有 exceptionally、也沒有 handle 或 whenComplete,錯誤會在到達下一次 get()/join() 呼叫前「悄然消失」,而該呼叫點可能離發生處很遠。
錯誤 №2:在主執行緒中未搭配 try-catch 就使用 get()/join()。 這會讓非同步程式碼變成同步,並可能造成阻塞或意外當機。
錯誤 №3:誤解處理器觸發的位置。 exceptionally 只會捕捉它之前的錯誤。如果在它之後再次發生錯誤,它不會處理這個錯誤。
錯誤 №4:有處理錯誤,但沒有回傳值。 在 exceptionally 或 handle 中務必回傳值,否則下一個階段會收到 null(或根本收不到任何東西)。
錯誤 №5:混淆 handle 與 whenComplete。 handle 可以改變結果,而 whenComplete 只能執行動作(例如記錄)。如果你想改變結果——請使用 handle。
錯誤 №6:重複撰寫錯誤處理邏輯。 通常可以把錯誤處理集中在一處,避免重複程式碼——例如使用集中式的 handle 或共用處理器。
GO TO FULL VERSION