1. 非同期タスクの起動: supplyAsync と runAsync
非同期タスクを起動する最も一般的な方法は CompletableFuture.supplyAsync を使うことです。このメソッドは結果を返すラムダやメソッドを受け取ります。例えば、サーバーからのデータ読み込みを模擬してみましょう:
import java.util.concurrent.CompletableFuture;
public class Main {
public static void main(String[] args) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// 長時間の処理を模擬(例: ファイルのダウンロード)
sleep(1000);
return "サーバーからのデータ";
});
System.out.println("タスクを開始しました!");
// ... タスクの実行中に、ここで他のことを行えます
}
private static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException ignored) {}
}
}
runAsync: 結果が不要な場合
タスクが何も返さない(例: ログ出力や通知送信)場合は、runAsync を使います:
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
sleep(500);
System.out.println("操作が完了しました!");
});
runAsync は常に CompletableFuture<Void> を返します。結果は想定されていないためです。
2. thenApply, thenAccept, thenRun: 何が違う?
非同期タスクが完了したら、その結果で何かをしたくなるのが普通です。そのための「ハンドラー」メソッドがあります:
- thenApply — 結果を変換し、新しい結果を返す。
- thenAccept — 結果を受け取るが値は返さない(副作用のために使う)。
- thenRun — 結果を受け取らず、値も返さない(タスク完了後に処理を実行するだけ)。
thenApply: 結果の処理と変換
前段のタスクの結果を変換したい場合は thenApply を使います。例えば、文字列を読み込んで、その長さを知りたいとします:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Java");
CompletableFuture<Integer> lengthFuture = future.thenApply(s -> {
System.out.println("文字列の長さを計算しています...");
return s.length();
});
// lengthFuture には文字列 "Java" の長さ(Integer)が入る
lengthFuture.thenAccept(len -> System.out.println("長さ: " + len));
何が起きているか:
- future には文字列 "Java" が入っている。
- thenApply は文字列をその長さ(int)に変換する。
- thenAccept は結果を出力する。
thenAccept: 結果に対する処理(値は返さない)
結果を画面に表示するなど、単に結果を処理したいが値を返す必要がない場合は thenAccept を使います:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "こんにちは、世界!");
future.thenAccept(result -> {
System.out.println("結果: " + result);
});
thenAccept は「コンシューマー」のようなもので、結果を受け取り、有用な処理を行います。
thenRun: 結果を使わない後続処理
タスク完了後に何らかの処理を実行したいが、結果は不要なときは thenRun を使います:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "完了!");
future.thenRun(() -> {
System.out.println("読み込みが完了しました!");
});
注意: thenRun の中からは前のタスクの結果にアクセスできません。結果は無視されます。
3. 呼び出しのチェーン: タスクでパイプラインを構築する
CompletableFuture の最大の強みは、計算のチェーンを構築できることです。各メソッド(thenApply、thenAccept、thenRun)は新しい CompletableFuture を返し、その先にさらにハンドラーを追加できます。
例: 多段の処理
例を拡張して、データを読み込み、変換し、結果を出力し、最後に完了をログに書きましょう。
CompletableFuture.supplyAsync(() -> {
System.out.println("ステップ1: データを読み込み中...");
sleep(500);
return "Java";
})
.thenApply(data -> {
System.out.println("ステップ2: データを変換中...");
return data.toUpperCase();
})
.thenAccept(result -> {
System.out.println("ステップ3: 結果を出力: " + result);
})
.thenRun(() -> {
System.out.println("ステップ4: すべて完了!");
});
コンソール出力:
ステップ1: データを読み込み中...
ステップ2: データを変換中...
ステップ3: 結果を出力: JAVA
ステップ4: すべて完了!
注意:
次の各ステップは、前のステップが完了してから開始されます。これにより、実用的なデータ処理のパイプラインを構築できます。
4. 非同期版: thenApplyAsync, thenAcceptAsync, thenRunAsync
デフォルトでは、ハンドラー(thenApply、thenAccept、thenRun)は前のタスクが完了したスレッドと同じスレッドで実行されます。処理が重い場合は、別スレッドに切り出すほうが適切です。
そのために非同期版があります:
- thenApplyAsync
- thenAcceptAsync
- thenRunAsync
何が違うのか?
- Async なし: ハンドラーは前のタスクと同じスレッドで実行される場合がある(例: タスクが ForkJoinPool で完了したなら、そのままそこで実行)。
- Async あり: ハンドラーは ForkJoinPool(またはあなたの Executor)内の別スレッドで実行されることが保証される。
例: 通常版と非同期版のハンドラーを比較
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
System.out.println("読み込み中... [" + Thread.currentThread().getName() + "]");
return "Hello";
});
future.thenApply(result -> {
System.out.println("thenApply: [" + Thread.currentThread().getName() + "]");
return result + " World";
});
future.thenApplyAsync(result -> {
System.out.println("thenApplyAsync: [" + Thread.currentThread().getName() + "]");
return result + " Async World";
});
典型的な出力:
読み込み中... [ForkJoinPool.commonPool-worker-1]
thenApply: [ForkJoinPool.commonPool-worker-1]
thenApplyAsync: [ForkJoinPool.commonPool-worker-2]
結論:
非同期ハンドラーは別スレッドで実行されます。
Async メソッドを使うべき場面
- 処理が重い場合(例: 複雑な計算、ネットワーク I/O)。
- 前のタスクが完了したスレッドをブロックしたくない場合。
- スレッドを明示的に管理したい場合(例: 第 2 引数として独自の Executor を渡す)。
5. 便利な細かな話題
表: thenApply, thenAccept, thenRun の比較
| メソッド | 結果を使う? | 値を返す? | 用途 |
|---|---|---|---|
|
はい | はい | 結果の変換 |
|
はい | いいえ | 副作用(出力、ログ) |
|
いいえ | いいえ | タスク完了後の単純な処理 |
|
はい | はい | 同じだが別スレッドで実行 |
|
はい | いいえ | 同じだが別スレッドで実行 |
|
いいえ | いいえ | 同じだが別スレッドで実行 |
質問: 長いチェーンはどう作る?
LEGO を組み立てるように、メソッドを次々に呼び出せます:
CompletableFuture.supplyAsync(() -> "42")
.thenApply(Integer::parseInt)
.thenApply(x -> x * 2)
.thenAccept(x -> System.out.println("結果: " + x));
出力:
結果: 84
各ステップは前の結果を受け取り、変更することも、そのまま利用することもできます。
6. thenApply/thenAccept/thenRun でよくあるミス
エラー1: 戻り値の型の取り違え。
thenApply はチェーンの先に渡す値を返す必要があります。誤って thenApply を使っておきながら値を返さないと、次の操作は null を受け取る(あるいはコンパイルエラーになる)ことがあります。副作用には thenAccept または thenRun を使いましょう。
エラー2: thenRun で結果を使おうとする。
thenRun の内部では前のタスクの結果にアクセスできません。結果を使いたいなら thenApply か thenAccept を選びましょう。
エラー3: メインスレッドのブロック。
メインスレッドで get() や join() を呼ぶと、非同期の利点を失います。スレッドはタスクの完了を待ってしまい、昔ながらの同期コードと同じになります。ブロックしないチェーンやコールバックを使いましょう。
エラー4: 例外を処理しない。
チェーンで例外が発生しても、exceptionally、handle、whenComplete といったハンドラーを追加していないと、その例外は「消えて」しまい、見えない失敗で終わることがあります。チェーンでは常にエラー処理を入れましょう。
エラー5: 思わぬ別スレッドでの実行。
非同期メソッド(thenApplyAsync など)は別スレッドで実行されることがあります。スレッドセーフでない変数にアクセスすると、データ競合が発生する可能性があります。
GO TO FULL VERSION