Java | Java 詳細・モダン文法:並行・非同期 – CompletableFuture 組み合わせ

Java Java
スポンサーリンク

CompletableFuture の「組み合わせ」をざっくり俯瞰する

CompletableFuture の本質は「非同期処理を“値”として扱い、それらをつなぎ合わせること」です。
単発で supplyAsync を使うだけだと「ただの非同期実行」で終わりますが、
本当におもしろくなるのは、複数の非同期処理を「順番につなぐ」「並列に走らせて結果を合わせる」といった“組み合わせ”をし始めてからです。

ここでは、代表的な組み合わせパターンを、
「順次」「並列」「エラー処理」という観点でかみ砕いていきます。


順次に処理をつなぐ:thenApply と thenCompose

thenApply:結果を「同期的に」変換する

thenApply は、
「前の非同期処理の結果を受け取って、普通の処理で変換する」ためのメソッドです。

CompletableFuture<Integer> future =
        CompletableFuture.supplyAsync(() -> 10)
                         .thenApply(x -> x * 2); // 10 → 20 に変換

Integer result = future.join(); // 20
Java

ここで起きていることは、

  1. supplyAsync で「10 を返す非同期処理」を作る
  2. その結果を thenApply で受け取り、x * 2 して返す

という「1 本の線の上での変換」です。
thenApply の中で返すのは「普通の値」であり、
新しい非同期処理ではありません。

thenCompose:非同期処理を「フラットにつなぐ」

thenCompose は、
「前の結果を使ってさらに別の非同期処理を始める」ときに使います。

CompletableFuture<String> future =
        CompletableFuture.supplyAsync(() -> "userId-123")
                         .thenCompose(id -> fetchUserNameAsync(id));

CompletableFuture<String> fetchUserNameAsync(String userId) {
    return CompletableFuture.supplyAsync(() -> "UserName of " + userId);
}
Java

もしここで thenApply(fetchUserNameAsync) としてしまうと、
型は CompletableFuture<CompletableFuture<String>> になってしまいます。

thenCompose は、
CompletableFuture<CompletableFuture<T>>CompletableFuture<T> に“平らにする”」
イメージです。

つまり、

  • thenApply は「値を変換する」
  • thenCompose は「非同期処理をつなげて、ネストを潰す」

という役割分担になっています。


並列に走らせて結果を合わせる:thenCombine と allOf

thenCombine:2 つの非同期結果を「合成」する

thenCombine は、
2 つの非同期処理を並列に走らせて、両方終わったら結果を合わせる」ためのメソッドです。

CompletableFuture<Integer> f1 =
        CompletableFuture.supplyAsync(() -> {
            sleep(1000);
            return 10;
        });

CompletableFuture<Integer> f2 =
        CompletableFuture.supplyAsync(() -> {
            sleep(800);
            return 20;
        });

CompletableFuture<Integer> combined =
        f1.thenCombine(f2, (x, y) -> x + y);

Integer result = combined.join(); // 30
Java

ここでのポイントは、

  • f1f2 は同時にスタートしている
  • 両方が終わったタイミングで (x, y) -> x + y が呼ばれる

という「2 本の線が合流する」形になっていることです。

「A と B を並列にやって、両方の結果が揃ったら C をする」
というパターンにぴったりです。

allOf:複数の非同期処理の「完了待ち」

CompletableFuture.allOf は、
複数の CompletableFuture が全部終わるのを待つ」ためのユーティリティです。

CompletableFuture<String> f1 = fetchAsync("A");
CompletableFuture<String> f2 = fetchAsync("B");
CompletableFuture<String> f3 = fetchAsync("C");

CompletableFuture<Void> all =
        CompletableFuture.allOf(f1, f2, f3);

all.join(); // ここで f1, f2, f3 の完了を待つ

String r1 = f1.join();
String r2 = f2.join();
String r3 = f3.join();
Java

allOf 自体は CompletableFuture<Void> を返すので、
個々の結果は元の f1, f2, f3 から取り出します。

「とにかく全部終わるのを待ちたい」
「終わったあとでそれぞれの結果をまとめて処理したい」
というときに使います。


どれか一つ終わればいい:anyOf

anyOf:最初に終わったものだけを使う

CompletableFuture.anyOf は、
複数の非同期処理のうち、最初に終わったものの結果だけを使う」ためのメソッドです。

CompletableFuture<String> slow = CompletableFuture.supplyAsync(() -> {
    sleep(2000);
    return "slow";
});

CompletableFuture<String> fast = CompletableFuture.supplyAsync(() -> {
    sleep(500);
    return "fast";
});

CompletableFuture<Object> any =
        CompletableFuture.anyOf(slow, fast);

Object result = any.join(); // "fast"
Java

ここでは、fast が先に終わるので、result"fast" になります。

「複数のサーバーに同じリクエストを投げて、一番早い応答を採用する」
「メイン経路とフォールバック経路のどちらか早い方を使う」
といったパターンに応用できます。

anyOf の戻り値は CompletableFuture<Object> なので、
実際にはキャストやジェネリクスの工夫が必要になりますが、
「最初に終わったやつだけ欲しい」という意図を表現するにはとても便利です。


エラーを含めて「流れ」として扱う:exceptionally と handle

exceptionally:失敗したときの「代替値」を返す

exceptionally は、
非同期処理が例外で失敗したときにだけ呼ばれるハンドラ」です。

CompletableFuture<Integer> future =
        CompletableFuture.supplyAsync(() -> {
            if (true) throw new RuntimeException("失敗");
            return 10;
        }).exceptionally(ex -> {
            System.out.println("エラー: " + ex.getMessage());
            return 0; // 代替値
        });

Integer result = future.join(); // 0
Java

成功した場合は exceptionally は呼ばれず、
失敗した場合だけ「ここでリカバリする」という形になります。

「失敗したらデフォルト値を返す」
「ログだけ出して、値は何かしら返す」
といったパターンに向いています。

handle:成功・失敗どちらも一箇所で扱う

handle は、
成功時の結果と、失敗時の例外の両方を受け取って処理する」メソッドです。

CompletableFuture<Integer> future =
        CompletableFuture.supplyAsync(() -> {
            if (Math.random() < 0.5) {
                return 10;
            } else {
                throw new RuntimeException("失敗");
            }
        }).handle((value, ex) -> {
            if (ex != null) {
                System.out.println("エラー: " + ex.getMessage());
                return 0;
            } else {
                return value * 2;
            }
        });

Integer result = future.join();
Java

handle の引数は (結果, 例外) の 2 つで、

  • 成功時:結果 に値が入り、例外null
  • 失敗時:結果null(または未定義)、例外 に Throwable

という形で渡されます。

「成功と失敗を一箇所でまとめて扱いたい」
「ログを出しつつ、成功時は変換、失敗時は代替値」
といった柔らかい制御ができます。


組み合わせの設計をどう考えるか

「線」と「合流」と「エラーの流れ」を意識する

CompletableFuture の組み合わせを設計するとき、
頭の中に「図」を描くと整理しやすくなります。

一本の線で順次つなぐところは thenApply / thenCompose
複数の線を合流させるところは thenCombine / allOf / anyOf
途中でエラーが起きたときの分岐は exceptionally / handle

こうやって、

「ここは A → B → C と順番につなぐ」
「ここで A と B を並列に走らせて、結果を合わせて C に渡す」
「ここで失敗したら、このルートに流す」

といった“フロー”を言葉にできると、
コードも自然とそれに沿った形になります。

thenApply と thenCompose を意識的に使い分ける

特に重要なのは、

  • 「ここはただの変換だから thenApply」
  • 「ここはさらに非同期処理を始めるから thenCompose」

と意識して書き分けることです。

thenApplyCompletableFuture を返してしまうと、
CompletableFuture<CompletableFuture<T>> という「二重の箱」になり、
扱いが一気に難しくなります。

「非同期処理をつなげるときは thenCompose」
という癖をつけておくと、
ネスト地獄を避けられます。


まとめ:CompletableFuture 組み合わせを自分の言葉で説明するなら

あなたの言葉でまとめると、こうなります。

CompletableFuture の本質は、非同期処理を“値”として扱い、それらを組み合わせること。
一本の線で順次つなぐときは、値の変換なら thenApply、さらに非同期処理を始めるなら thenCompose
複数の非同期処理を並列に走らせて結果を合わせるときは、2 つなら thenCombine、たくさんなら allOf、どれか一つでよければ anyOf
エラーを流れの中で扱うには、失敗時だけを扱う exceptionally、成功・失敗両方を一箇所で扱う handle を使う。

大事なのは、コードを書く前に
『ここは順次? 並列? どこで合流? 失敗したらどこへ?』
という“フローの絵”を頭の中に描き、それを CompletableFuture の組み合わせで表現すること。」

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