CodeGym /課程 /JAVA 25 SELF /組合 CompletableFuture:thenCombine、allOf、anyOf

組合 CompletableFuture:thenCombine、allOf、anyOf

JAVA 25 SELF
等級 55, 課堂 2
開放

1. 組合 CompletableFuture

在真實系統中,資料很少只來自單一來源:可以並行載入使用者的個人檔案與其訂單,將來自兩個微服務的資料合併,有時也只想處理最先返回的結果。同步作法會讓你逐一等待;CompletableFuture 允許同時啟動並優雅地組合結果。它提供了專用方法:thenCombineallOfanyOf。讓我們依序看看。

平行任務:兩個非同步請求

同步的情況會是這樣:

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,兩個任務都必須成功完成,否則會拋出例外。

方法比較表

方法 適用時機 結果型別
thenCombine
需要合併兩個任務的結果 合併後的結果
allOf
需要等待所有任務完成
Void
anyOf
需要等待任一任務完成 Object(第一個完成的任務之結果)

6. 組合 CompletableFuture 時的常見錯誤

錯誤 1:在主執行緒中用 get()/join() 等待結果。
如果你寫的是非同步程式碼,最後卻仍然呼叫 get()join(),你就是在阻塞執行緒,等於放棄了非同步的優勢。建議改用 thenAccept/thenRun 以非阻塞方式處理結果。

錯誤 2:使用 allOf 時沒有保留原始 futures 的參考。
如果你呼叫了 CompletableFuture.allOf(f1, f2, f3),卻沒有保存 f1f2f3 — 就無法取得它們的結果。allOf 只會回傳 Void

錯誤 3:沒有在鏈式中處理錯誤。
如果某個任務以錯誤結束,整個 allOfthenCombine 也會以錯誤結束。請使用錯誤處理方法(exceptionallyhandlewhenComplete),以免漏掉例外。

錯誤 4:anyOf 的型別不一致。
anyOf 會回傳 Object。如果你的 future 回傳不同型別,就得判斷先來的是哪一種。若可行,最好讓所有任務的型別一致。

錯誤 5:鏈式過於複雜且沒有註解。
當程式碼變多時,future 的鏈式可能變成「麵條程式」。不要猶豫,把鏈式拆成中間變數,並為每個步驟加上註解。

1
任務
JAVA 25 SELF, 等級 55, 課堂 2
上鎖
貨運協調:等待所有貨物
貨運協調:等待所有貨物
1
任務
JAVA 25 SELF, 等級 55, 課堂 2
上鎖
組合問候:合併語句片段
組合問候:合併語句片段
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION