Java | Java 詳細・モダン文法:並行・非同期 – thenApply / thenCompose

Java Java
スポンサーリンク

まず「何をつなぎたいのか」をはっきりさせる

CompletableFuture を使うとき、
thenApplythenCompose の違いで必ずつまずきます。

でも本質はシンプルで、こうです。

  • ただ結果を変換したいだけ」なら thenApply
  • 結果を使って、さらに別の非同期処理を始めたい」なら thenCompose

この一行を、頭にベタッと貼っておいてください。
あとは「箱が増えるかどうか」の話に過ぎません。


thenApply の役割:結果を「普通の値」として変換する

イメージ:非同期の“あと”で、ただの map をしているだけ

thenApply は、
「非同期処理の結果に対して、同期的な変換を 1 回かける」メソッドです。

コードで見るとイメージしやすいです。

CompletableFuture<Integer> future =
        CompletableFuture.supplyAsync(() -> 10)   // 非同期で 10 を計算
                         .thenApply(x -> x * 2);  // 結果を 2 倍にする

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

ここで起きていることは、
「非同期で 10 を手に入れて、その“あとで” x * 2 という普通の処理をしている」だけです。

thenApply の中で返しているのは「ただの値(ここでは x * 2 の結果)」であって、
新しい CompletableFuture ではありません。

つまり、型の流れはこうです。

CompletableFuture<Integer>
thenApply(x -> x * 2)
CompletableFuture<Integer>(中身の値だけ変わる)

非同期の「箱」は増えず、中身だけ変わる。
これが thenApply の世界です。

もう少しだけ複雑な例

例えば、ユーザー ID から表示用のメッセージを作る。

CompletableFuture<String> future =
        CompletableFuture.supplyAsync(() -> "user-123")      // 非同期で ID を取得
                         .thenApply(id -> "Hello, " + id);   // 文字列を加工

String msg = future.join(); // "Hello, user-123"
Java

ここでも、
「ID を取るところだけが非同期」で、
その後の "Hello, " + id は普通の同期処理です。

「非同期の結果に対して、ちょっとした加工をしたい」
そんなときは、まず thenApply を思い出してください。


thenCompose の役割:結果を使って「さらに非同期」を始める

イメージ:非同期の“あと”に、もう一個非同期をつなぐ

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

例えば、こういう流れを考えます。

  1. 非同期で userId を取得する
  2. その userId を使って、非同期でユーザー情報を取得する

コードで書くとこうなります。

CompletableFuture<String> userIdFuture =
        CompletableFuture.supplyAsync(() -> "user-123");

CompletableFuture<String> userNameFuture =
        userIdFuture.thenCompose(id -> fetchUserNameAsync(id));

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

ここで重要なのは、fetchUserNameAsync
String ではなく CompletableFuture<String> を返している」ことです。

つまり、thenCompose の中では、
「普通の値」ではなく「新しい非同期処理(CompletableFuture)」を返しています。

thenApply で書くと「箱が二重になる」問題

同じことを thenApply で書いてしまうと、こうなります。

CompletableFuture<CompletableFuture<String>> nested =
        userIdFuture.thenApply(id -> fetchUserNameAsync(id));
Java

型に注目してください。

CompletableFuture<CompletableFuture<String>>

「非同期の箱の中に、さらに非同期の箱が入っている」状態です。
これを扱うのは、かなり面倒です。

thenCompose は、
この「二重の箱」を「一重の箱」に“平らにする(compose = 合成する)”ためのメソッドです。

つまり、

  • thenApplyCompletableFuture<T> を返すと、箱が二重になる
  • thenCompose を使うと、その二重を一重にしてくれる

という違いがあります。


「箱の数」で直感的に見分ける

ルール:戻り値が「普通の値」か「CompletableFuture」か

thenApplythenCompose を選ぶとき、
自分にこう問いかけてください。

「このラムダの戻り値は、
普通の値(T) か?
それとも CompletableFuture<T> か?」

普通の値なら thenApply
CompletableFuture なら thenCompose

これだけです。

もう少し具体的に書くと、

// thenApply のイメージ
.thenApply(x -> {
    // x を使って計算して、普通の値を返す
    return x * 2;              // 戻り値は T
})

// thenCompose のイメージ
.thenCompose(x -> {
    // x を使って、さらに非同期処理を始める
    return someAsync(x);       // 戻り値は CompletableFuture<T>
})
Java

「非同期を増やしているかどうか」で判断するのではなく、
「戻り値の型が何か」で判断すると、迷いが減ります。


thenApply と thenCompose を混ぜた実用的な例

シナリオ:ID → ユーザー情報 → 表示用メッセージ

流れとしてはこうです。

  1. 非同期で userId を取得する
  2. その userId を使って、非同期でユーザー情報を取得する
  3. 取得したユーザー情報を、同期的にメッセージに整形する

コードに落とすとこうなります。

CompletableFuture<String> messageFuture =
        CompletableFuture.supplyAsync(() -> "user-123")          // 1. ID を非同期取得
                         .thenCompose(id -> fetchUserAsync(id))  // 2. さらに非同期でユーザー取得
                         .thenApply(user ->                      // 3. 同期的に整形
                                 "Hello, " + user.getName());

CompletableFuture<User> fetchUserAsync(String userId) {
    return CompletableFuture.supplyAsync(() -> {
        sleep(500);
        return new User(userId, "Taro");
    });
}
Java

ここでのポイントは、

ID → ユーザー情報:非同期 → 非同期 なので thenCompose
ユーザー情報 → メッセージ:非同期 → 同期処理 なので thenApply

というふうに、
「どこで非同期をつなぎ、どこで普通の変換をしているか」が
コードからはっきり読み取れることです。


よくある間違いと、その直し方

間違いパターン:全部 thenApply で書いてしまう

例えば、こんなコード。

CompletableFuture<CompletableFuture<User>> future =
        CompletableFuture.supplyAsync(() -> "user-123")
                         .thenApply(id -> fetchUserAsync(id)); // ここ
Java

これだと、型が CompletableFuture<CompletableFuture<User>> になってしまいます。
このあと join() を 2 回呼んだりして、コードがどんどん気持ち悪くなります。

直し方はシンプルで、
「ここは非同期をつなぎたいんだから thenCompose だよね」と書き換えるだけです。

CompletableFuture<User> future =
        CompletableFuture.supplyAsync(() -> "user-123")
                         .thenCompose(id -> fetchUserAsync(id));
Java

「箱が二重になっていたら、thenCompose を疑う」
という感覚を持っておくと、デバッグが楽になります。


まとめ:thenApply / thenCompose を自分の言葉で説明するなら

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

thenApply は、非同期処理の結果を“普通の値”として受け取り、
それを同期的に変換するためのメソッド。
箱(CompletableFuture)の中身だけを変えるイメージで、
型は CompletableFuture<T>CompletableFuture<U> のまま。

thenCompose は、非同期処理の結果を使って“さらに別の非同期処理”を始めるときに使うメソッド。
CompletableFuture<T> を受けて CompletableFuture<U> を返す処理を、
CompletableFuture<CompletableFuture<U>> ではなく
CompletableFuture<U> に“平らにして”つないでくれる。

判断基準は、
『このラムダの戻り値は普通の値か? CompletableFuture か?』
普通の値なら thenApply、CompletableFuture なら thenCompose。」

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