導入:なぜ非同期処理の「つなぎ方」が重要なのか
Javaで非同期処理を行う際、単に「別スレッドで処理を実行する」だけでは不十分です。実務では「処理Aが終わったら、その結果を使って処理Bを行い、最後に結果を画面に表示する」といった連続したタスクの制御が必要になります。CompletableFutureのthenApply, thenAccept, thenRunは、こうした非同期タスクの連鎖をスマートに書くための重要なメソッドです。これらを使いこなすことで、複雑なコールバック地獄を避け、読みやすく効率的なコードが書けるようになります。
基礎知識:3つのメソッドの違いを理解する
CompletableFutureは、非同期処理の結果を待機し、完了した後に次のアクションを繋げる機能を持っています。それぞれのメソッドには明確な役割があります。
thenApply: 結果を変換して「次」に渡したい時に使います。関数型インターフェースFunctionを受け取り、戻り値を返します。
thenAccept: 結果を受け取って「何か(出力や保存など)」をしたい時に使います。戻り値は返しません(Consumer)。
thenRun: 結果には興味がなく、前の処理が終わったタイミングで「ただ何かを実行したい」時に使います。Runnableを受け取ります。
実装/解決策:タスクのパイプライン化
非同期処理を構築する際は、処理の結果が必要か、戻り値が必要かを基準にメソッドを選択します。基本的には「変換ならApply」「消費ならAccept」「完了通知ならRun」と覚えましょう。
サンプルプログラム
以下のコードをコピー&ペーストして、非同期処理の流れを確認してみてください。
import java.util.concurrent.CompletableFuture;
public class AsyncDemo {
public static void main(String[] args) {
// 1. 非同期処理を開始
CompletableFuture
return “Javaエンジニア”;
});
// 2. thenApply: 結果を加工して次に渡す
CompletableFuture
// 3. thenAccept: 加工された結果を使って何かをする(戻り値なし)
transformed.thenAccept(result -> {
System.out.println(“処理結果: ” + result);
});
// 4. thenRun: 最後に何かが終わったことを通知する
transformed.thenRun(() -> {
System.out.println(“全ての処理が完了しました!”);
});
// メインスレッドが先に終了しないように待機
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
}
}
応用・注意点:現場で陥りやすい罠
実務でこれらのメソッドを使う際に注意すべきポイントが3つあります。
1. 例外処理: 非同期処理内で例外が発生した場合、thenApply等のチェーンは中断されます。必ず最後にexceptionallyメソッドを使ってエラーハンドリングを行いましょう。
2. 実行スレッドの意識: デフォルトではForkJoinPoolが使われますが、重いI/O処理を含む場合は、専用のExecutorServiceを渡すことで、メインの処理に影響を与えないように設計するのが定石です。
3. Virtual Threadsとの関係: Java 21以降で導入されたVirtual Threadsは、スレッドを軽量化しますが、CompletableFutureのチェーンは依然として「非同期プログラミングの構造」として非常に有用です。構造化並行処理(Structured Concurrency)と組み合わせることで、より安全で保守性の高い非同期コードが実現できます。
まずは簡単な処理からこれらのメソッドを使い分け、非同期処理の設計に慣れていきましょう。

コメント