【Java学習|豆知識】CompletableFutureの例外処理と完了通知をマスターしよう:exceptionally、handle、whenCompleteの使い分け

1. 導入:なぜ非同期処理の例外ハンドリングが重要なのか

Javaで非同期処理を行う際、CompletableFutureは非常に強力なツールです。しかし、非同期で実行されているタスクで例外が発生した場合、メインスレッド側でそれを適切にキャッチできず、エラーが握りつぶされてしまうという課題があります。システムを堅牢にするためには、非同期処理の完了時や失敗時にどのようなアクションを取るかを明示的に定義することが不可欠です。

2. 基礎知識:3つのメソッドの役割

CompletableFutureには、非同期処理の完了後に実行されるメソッドがいくつか存在します。それぞれの役割を整理しましょう。

exceptionally():例外が発生したときのみ実行され、代替の値(デフォルト値)を返します。
whenComplete():成功・失敗に関わらず実行されますが、結果を変換することはできません。主にログ出力や後始末に使います。
handle():成功・失敗の両方を受け取り、結果を加工したり、別の値に変換したりして次へ渡すことができます。

3. 実装と解決策

非同期処理のパイプラインを作成する際は、処理の途中で「値を変換したいのか」あるいは「失敗時のリカバリをしたいのか」を基準にメソッドを選定します。

・リカバリが必要な場合:exceptionally
・計算結果を次の処理へ繋げつつ、エラーならデフォルト値へ変換する場合:handle
・副作用(ログ出力など)のみ行いたい場合:whenComplete

4. サンプルプログラム

以下のコードは、各メソッドの挙動を比較する実用的な例です。

import java.util.concurrent.CompletableFuture;

public class AsyncExample {
    public static void main(String[] args) {
        // 例外が発生する可能性のある非同期処理
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            if (Math.random() > 0.5) throw new RuntimeException("計算失敗!");
            return 100;
        });

        // 1. handle: 結果と例外の両方をハンドリングし、値を変換する
        CompletableFuture<Integer> handled = future.handle((result, ex) -> {
            if (ex != null) {
                System.out.println("handleでエラー検知: " + ex.getMessage());
                return 0; // エラー時は0を返す
            }
            return result + 50;
        });

        // 2. whenComplete: 結果の加工はせず、ログ出力などの副作用を実行
        handled.whenComplete((res, ex) -> {
            if (ex == null) System.out.println("最終結果: " + res);
            else System.out.println("処理は終了しました。");
        });

        // 3. exceptionally: エラー時のみの代替値提供
        CompletableFuture<Integer> safeFuture = CompletableFuture.supplyAsync(() -> {
            throw new RuntimeException("緊急停止");
        }).exceptionally(ex -> {
            System.out.println("exceptionallyでリカバリ");
            return -1;
        });

        safeFuture.thenAccept(val -> System.out.println("値: " + val));
        
        // メインスレッド終了待ち
        try { Thread.sleep(1000); } catch (InterruptedException e) {}
    }
}

5. 応用・注意点

現場での開発で陥りやすい罠として、whenComplete内で発生した例外が呼び出し元に伝播しにくいという点があります。また、Java 21以降の「仮想スレッド(Virtual Threads)」を利用する場合、CompletableFuture内でのブロッキング処理は避けるべきです。

さらに、複雑な非同期パイプラインでは、どのメソッドがどの例外を処理しているのか追跡が困難になりがちです。可能な限りパイプラインの最後で集約して例外処理を行うか、あるいはStructured Concurrency(構造化並行処理)を活用し、スレッドのライフサイクルと例外のスコープを一致させる設計を検討してください。これらを適切に使い分けることで、可読性の高い非同期コードが実現できます。

コメント

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