【プログラミングしてみよう43】JavaのCompletableFuture入門(非同期処理)

Java

前回はsynchronizedとvolatileを使ったスレッドセーフなコードの書き方を学びました。

今回は「CompletableFuture」について解説します。 Java 8から導入された CompletableFuture を使うことで、時間のかかる処理を非同期で実行しながら、結果が揃ったら次の処理を行うといった「非同期処理」を直感的に書けるようになります。

非同期処理とは

これまでのプログラムは基本的に「上から順に1行ずつ実行する」同期処理でした。たとえばAPIからデータを取得する処理に3秒かかる場合、その間プログラムは何もできず待ち続けます。

「非同期処理」を使うと、時間のかかる処理をバックグラウンドで実行しながら、メインスレッドは別の処理を続けることができます。処理が完了したらその結果を受け取って次の処理を行うという流れを、CompletableFuture を使ってシンプルに書けます。

// 同期処理(順番に実行)
String result1 = fetchData();   // 3秒待つ
String result2 = fetchData();   // さらに3秒待つ
// 合計6秒かかる

// 非同期処理(並行して実行)
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> fetchData());
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> fetchData());
// 両方同時に実行されるので合計約3秒で済む

サンプルコード(CompletableFutureの基本)

ファイル作成

「Java31」フォルダに CompletableTest.java という名前でファイルを作成します

コード入力

以下のコードを入力します。

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class CompletableTest {
    // 時間のかかる処理をシミュレート
    static String fetchUserName(int userId) {
        try {
            Thread.sleep(1000); // 1秒かかる処理
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return "ユーザー" + userId;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        System.out.println("処理開始");
        long start = System.currentTimeMillis();

        // 非同期で実行
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> fetchUserName(1));

        System.out.println("非同期処理中...他の作業ができます");

        // 結果を取得(完了まで待つ)
        String result = future.get();
        long elapsed = System.currentTimeMillis() - start;

        System.out.println("結果: " + result);
        System.out.println("経過時間: " + elapsed + "ms");

        // thenApplyで結果を変換する
        CompletableFuture<String> pipeline = CompletableFuture
                .supplyAsync(() -> fetchUserName(2))
                .thenApply(name -> name + "さん、こんにちは!")
                .thenApply(String::toUpperCase);

        System.out.println("\nパイプライン結果: " + pipeline.get());
    }
}

コンパイル

javac CompletableTest.java

実行

java CompletableTest

実行結果は次のようになります。

処理開始
非同期処理中...他の作業ができます
結果: ユーザー1
経過時間: 1055ms
パイプライン結果: ユーザー2さん、こんにちは!

サンプルコード(複数の非同期処理の組み合わせ)

ファイル作成

「Java31」フォルダに CompletableAllTest.java という名前でファイルを作成します

コード入力

以下のコードを入力します。

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class CompletableAllTest {
    static String fetchData(String source, int delayMs) {
        try {
            Thread.sleep(delayMs);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return source + "のデータ";
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        long start = System.currentTimeMillis();

        // 3つの処理を並行して実行
        CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> fetchData("API-A", 1000));
        CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> fetchData("API-B", 1500));
        CompletableFuture<String> f3 = CompletableFuture.supplyAsync(() -> fetchData("API-C", 800));

        // すべての処理が完了するのを待つ
        CompletableFuture<Void> allOf = CompletableFuture.allOf(f1, f2, f3);
        allOf.get();

        long elapsed = System.currentTimeMillis() - start;

        System.out.println("結果1: " + f1.get());
        System.out.println("結果2: " + f2.get());
        System.out.println("結果3: " + f3.get());
        System.out.println("合計経過時間: " + elapsed + "ms");
        System.out.println("(同期で実行した場合は約3300msかかる)");

        // エラーハンドリング
        CompletableFuture<String> withError = CompletableFuture
                .supplyAsync(() -> {
                    if (true) throw new RuntimeException("API接続エラー");
                    return "成功";
                })
                .exceptionally(e -> "エラーが発生しました: " + e.getMessage());

        System.out.println("\nエラーハンドリング: " + withError.get());
    }
}

コンパイル

javac CompletableAllTest.java

実行

java CompletableAllTest

実行結果は次のようになります。

結果1: API-Aのデータ
結果2: API-Bのデータ
結果3: API-Cのデータ
合計経過時間: 1530ms
(同期で実行した場合は約3300msかかる)

エラーハンドリング: エラーが発生しました: java.lang.RuntimeException: API接続エラー

コードの解説

CompletableFuture.supplyAsync(() -> ...) は、ラムダ式の処理をバックグラウンドスレッドで非同期に実行します。supplyAsync は処理の結果を返す場合に使い、結果を返さない場合は runAsync を使います。

future.get() で処理の完了を待ち、結果を取り出します。処理が完了するまでここでブロック(待機)します。get() の前に他の処理を書くことで、非同期処理の完了を待ちながら別の作業も並行して進められます。

thenApply() は処理が完了した後に続けて別の処理を行うメソッドです。メソッドチェーンでつなげることで「取得→加工→変換」のような一連の流れをパイプライン状に書けます。前回学んだStream APIの map() と似た考え方です。

CompletableFuture.allOf(f1, f2, f3) は複数の CompletableFuture をまとめて、すべてが完了するまで待ちます。3つの処理が合計3300msかかるところを、並行実行により最も時間のかかる1500ms程度で終わっているのが実行結果からわかります。

exceptionally() は処理中に例外が発生した場合の代替値を返すメソッドです。以前学んだtry-catchの非同期版のようなもので、エラーが発生してもパイプラインが途切れずに代替値を返し続けます。

まとめ

CompletableFuture を使うことで、時間のかかる処理を非同期で実行しながら、結果を使った後続処理をメソッドチェーンでシンプルに書けます。supplyAsync で非同期処理を開始し、thenApply で結果を加工し、allOf で複数の処理をまとめて待ち、exceptionally でエラーに対応するという基本の流れを覚えておきましょう。APIの呼び出しや重い計算処理を並行して実行したい場面で特に力を発揮します。これまで学んだラムダ式・Stream APIと同じ感覚で使えるため、組み合わせて活用していきましょう。

2026/07/22(水) ぱんまる

コメント

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