【Java学習|実務向け】CompletableFutureのthenComposeとthenCombineを使いこなす:非同期処理の連結と合成

導入

Javaの非同期処理において、複数のタスクを効率的に組み合わせることは、スループット向上に直結します。特にCompletableFutureは、従来のFutureよりも柔軟な非同期プログラミングを可能にしますが、処理の「連結(Compose)」と「合成(Combine)」の使い分けに悩むエンジニアは少なくありません。本記事では、これらを適切に使い分け、ブロッキングを避けるための実装パターンを解説します。

基礎知識

CompletableFutureは、非同期処理の結果を待機せずに次のアクションを定義するための仕組みです。
thenComposeは、前のタスクの結果を使って「別の非同期処理(CompletableFuture)」を開始したい場合に使用します。これは、入れ子になったCompletableFutureのフラット化(ネストの解消)に役立ちます。
thenCombineは、2つの独立した非同期処理の結果を待ち合わせ、それらを組み合わせて新しい結果を生成したい場合に使用します。

実装/解決策

実務では、以下のように使い分けます。
thenCompose: 処理Aの結果を入力として処理Bを呼び出す「依存関係」がある場合。
thenCombine: 処理Aと処理Bが独立しており、両方の結果をマージして出力を作成したい場合。

サンプルプログラム

以下は、ユーザー情報を取得した後に、そのユーザーの注文履歴を取得するケース(thenCompose)と、並行して取得した複数の情報を集約するケース(thenCombine)のサンプルです。

import java.util.concurrent.CompletableFuture;

public class AsyncDemo {
    public static void main(String[] args) {
        // 1. thenCompose: 依存関係のある処理
        // ユーザーIDから詳細を取得し、その結果を使って注文履歴を取得する
        CompletableFuture<String> resultCompose = fetchUserId()
            .thenCompose(userId -> fetchOrderHistory(userId));

        // 2. thenCombine: 独立した処理の合成
        // ユーザー情報と商品情報を並行して取得し、最後に組み合わせる
        CompletableFuture<String> resultCombine = fetchUser()
            .thenCombine(fetchProduct(), (user, product) -> user + " が " + product + " を購入");

        System.out.println(resultCombine.join());
    }

    private static CompletableFuture<String> fetchUserId() {
        return CompletableFuture.supplyAsync(() -> "User123");
    }

    private static CompletableFuture<String> fetchOrderHistory(String userId) {
        return CompletableFuture.supplyAsync(() -> "Order_for_" + userId);
    }

    private static CompletableFuture<String> fetchUser() {
        return CompletableFuture.supplyAsync(() -> "山田太郎");
    }

    private static CompletableFuture<String> fetchProduct() {
        return CompletableFuture.supplyAsync(() -> "Java入門書");
    }
}

応用・注意点

1. スレッドプールの明示的指定: CompletableFutureのメソッドは、デフォルトでForkJoinPool.commonPool()を使用します。ブロッキングIOが発生する処理が含まれる場合、必ず自前のExecutorServiceや、Java 21以降のVirtual Threads(Executors.newVirtualThreadPerTaskExecutor())を指定してください。
2. 例外処理: 連結された処理の途中で例外が発生すると、以降の処理がスキップされます。必ずhandle()やexceptionally()を使用して、エラーハンドリングをチェインの末尾に組み込んでください。
3. Structured Concurrencyとの比較: Java 21以降では、より安全な並行処理のためにStructured Concurrency(StructuredTaskScope)の利用が推奨されます。単純なタスクの連結にはCompletableFutureが適していますが、複雑な並行ライフサイクル管理が必要な場合は、Structured Concurrencyへの移行を検討しましょう。

コメント

タイトルとURLをコピーしました