1. 組合 CompletableFuture
在真實系統中,資料很少只來自單一來源:可以並行載入使用者的個人檔案與其訂單,將來自兩個微服務的資料合併,有時也只想處理最先返回的結果。同步作法會讓你逐一等待;CompletableFuture 允許同時啟動並優雅地組合結果。它提供了專用方法:thenCombine、allOf、anyOf。讓我們依序看看。
平行任務:兩個非同步請求
同步的情況會是這樣:
String name = loadUserName(); // 很慢
int balance = loadUserBalance(); // 很慢
System.out.println("姓名: " + name + ", 餘額: " + balance);
問題:第二個呼叫只會在第一個完成後才開始。
非同步方式
使用 CompletableFuture 可以同時啟動兩個任務:
CompletableFuture<String> nameFuture = CompletableFuture.supplyAsync(() -> loadUserName());
CompletableFuture<Integer> balanceFuture = CompletableFuture.supplyAsync(() -> loadUserBalance());
但要如何同時取得兩個結果並一起處理?這就用到 thenCombine。
2. thenCombine:合併兩個任務的結果
方法 thenCombine 可將兩個 CompletableFuture 合併,當兩個任務都完成時執行動作。它會回傳一個包含合併結果的新 CompletableFuture。
方法簽名:
<A, B, C> CompletableFuture<C> thenCombine(
CompletionStage<? extends B> other,
BiFunction<? super A, ? super B, ? extends C> fn
)
- A — 第一個 future 的結果型別;
- B — 第二個的型別;
- C — 合併後的結果型別。
範例
CompletableFuture<String> nameFuture = CompletableFuture.supplyAsync(() -> loadUserName());
CompletableFuture<Integer> balanceFuture = CompletableFuture.supplyAsync(() -> loadUserBalance());
CompletableFuture<String> resultFuture = nameFuture.thenCombine(
balanceFuture,
(name, balance) -> "姓名: " + name + ", 餘額: " + balance
);
resultFuture.thenAccept(System.out::println);
運作方式:
- 兩個 future 會並行啟動。
- 一旦兩者都完成,就會呼叫函式 (name, balance) -> ...。
- 最終的 future 會包含結果字串。
數字的小範例
CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> 2);
CompletableFuture<Integer> f2 = CompletableFuture.supplyAsync(() -> 3);
CompletableFuture<Integer> sum = f1.thenCombine(f2, Integer::sum);
sum.thenAccept(result -> System.out.println("總和: " + result));
輸出:
總和: 5
非同步變體
若合併是繁重的操作,請使用 thenCombineAsync:
f1.thenCombineAsync(f2, (a, b) -> a * b);
3. allOf:當任務很多時
如果不是兩個任務,而是一整打怎麼辦?例如,我們想並行載入 10 位使用者的資料。 為此可使用 CompletableFuture.allOf。
說明
CompletableFuture.allOf(f1, f2, ..., fn) 會回傳一個新的 future,當且僅當所有傳入的任務都完成時才會完成。但有個細節:這個 future 不包含結果 — 它的型別永遠是 CompletableFuture<Void>。若要取得結果,需要另外從原本的 futures 中「取出」。
範例
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> "第一");
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> "第二");
CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2);
all.thenRun(() -> {
// 所有任務都完成!
String s1 = f1.join(); // join() — 類似 get(),但會拋出未檢查例外
String s2 = f2.join();
System.out.println(s1 + " & " + s2);
});
輸出:
第一 & 第二
含任務陣列的範例
List<CompletableFuture<String>> futures = new ArrayList<>();
for (int i = 0; i < 5; i++) {
int id = i;
futures.add(CompletableFuture.supplyAsync(() -> "使用者 " + id));
}
CompletableFuture<Void> all = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
all.thenRun(() -> {
for (CompletableFuture<String> f : futures) {
System.out.println(f.join());
}
});
發生了什麼:
- 所有任務會並行啟動。
- allOf 會在所有任務完成時才完成。
- 在 thenRun 區塊中,我們可以透過 join() 取得結果。
示意圖
[Future1] \
[Future2] ----> [allOf] ---> thenRun
[Future3] /
4. anyOf:等待第一個完成的任務
有時不需要等全部,而是取最先完成的結果。比方同時向兩個伺服器請求資料 — 誰先回就用誰的資料。 這時可使用 CompletableFuture.anyOf。
說明
CompletableFuture.anyOf(f1, f2, ..., fn) 會回傳一個 future,當任一傳入任務完成時就會完成。結果型別是 CompletableFuture<Object>,因為各任務的型別可能不同。
範例
CompletableFuture<String> fast = CompletableFuture.supplyAsync(() -> {
sleep(500);
return "快速伺服器";
});
CompletableFuture<String> slow = CompletableFuture.supplyAsync(() -> {
sleep(2000);
return "慢速伺服器";
});
CompletableFuture<Object> any = CompletableFuture.anyOf(fast, slow);
any.thenAccept(result -> System.out.println("收到: " + result));
輸出:
收到: 快速伺服器
不同型別的範例
可以組合不同型別的任務,但此時結果會是 Object,需要顯式轉型。
5. 實用細節
建議
- allOf 不會回傳結果陣列。必須保留原始的 futures,才能透過 join() 或 get() 取得其值。
- anyOf 回傳第一個完成的結果,但型別是 Object。若所有任務型別一致,可以進行轉型。
- 若 allOf 中有任務以錯誤結束,最終的 future 也會以錯誤結束。
- 對於 thenCombine,兩個任務都必須成功完成,否則會拋出例外。
方法比較表
| 方法 | 適用時機 | 結果型別 |
|---|---|---|
|
需要合併兩個任務的結果 | 合併後的結果 |
|
需要等待所有任務完成 | |
|
需要等待任一任務完成 | Object(第一個完成的任務之結果) |
6. 組合 CompletableFuture 時的常見錯誤
錯誤 1:在主執行緒中用 get()/join() 等待結果。
如果你寫的是非同步程式碼,最後卻仍然呼叫 get() 或 join(),你就是在阻塞執行緒,等於放棄了非同步的優勢。建議改用 thenAccept/thenRun 以非阻塞方式處理結果。
錯誤 2:使用 allOf 時沒有保留原始 futures 的參考。
如果你呼叫了 CompletableFuture.allOf(f1, f2, f3),卻沒有保存 f1、f2、f3 — 就無法取得它們的結果。allOf 只會回傳 Void!
錯誤 3:沒有在鏈式中處理錯誤。
如果某個任務以錯誤結束,整個 allOf 或 thenCombine 也會以錯誤結束。請使用錯誤處理方法(exceptionally、handle、whenComplete),以免漏掉例外。
錯誤 4:anyOf 的型別不一致。
anyOf 會回傳 Object。如果你的 future 回傳不同型別,就得判斷先來的是哪一種。若可行,最好讓所有任務的型別一致。
錯誤 5:鏈式過於複雜且沒有註解。
當程式碼變多時,future 的鏈式可能變成「麵條程式」。不要猶豫,把鏈式拆成中間變數,並為每個步驟加上註解。
GO TO FULL VERSION