まず「何をつなぎたいのか」をはっきりさせる
CompletableFuture を使うとき、thenApply と thenCompose の違いで必ずつまずきます。
でも本質はシンプルで、こうです。
- 「ただ結果を変換したいだけ」なら
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 は、
「前の結果を使って、さらに別の非同期処理を始める」ときに使います。
例えば、こういう流れを考えます。
- 非同期で userId を取得する
- その 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 = 合成する)”ためのメソッドです。
つまり、
thenApplyでCompletableFuture<T>を返すと、箱が二重になるthenComposeを使うと、その二重を一重にしてくれる
という違いがあります。
「箱の数」で直感的に見分ける
ルール:戻り値が「普通の値」か「CompletableFuture」か
thenApply と thenCompose を選ぶとき、
自分にこう問いかけてください。
「このラムダの戻り値は、
普通の値(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 → ユーザー情報 → 表示用メッセージ
流れとしてはこうです。
- 非同期で userId を取得する
- その userId を使って、非同期でユーザー情報を取得する
- 取得したユーザー情報を、同期的にメッセージに整形する
コードに落とすとこうなります。
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。」
