CodeGym /コース /JAVA 25 SELF /非同期タスク: thenApply, thenAccept, thenRun

非同期タスク: thenApply, thenAccept, thenRun

JAVA 25 SELF
レベル 55 , レッスン 1
使用可能

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 の最大の強みは、計算のチェーンを構築できることです。各メソッド(thenApplythenAcceptthenRun)は新しい 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

デフォルトでは、ハンドラー(thenApplythenAcceptthenRun)は前のタスクが完了したスレッドと同じスレッドで実行されます。処理が重い場合は、別スレッドに切り出すほうが適切です。

そのために非同期版があります:

  • 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 の比較

メソッド 結果を使う? 値を返す? 用途
thenApply
はい はい 結果の変換
thenAccept
はい いいえ 副作用(出力、ログ)
thenRun
いいえ いいえ タスク完了後の単純な処理
thenApplyAsync
はい はい 同じだが別スレッドで実行
thenAcceptAsync
はい いいえ 同じだが別スレッドで実行
thenRunAsync
いいえ いいえ 同じだが別スレッドで実行

質問: 長いチェーンはどう作る?

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 の内部では前のタスクの結果にアクセスできません。結果を使いたいなら thenApplythenAccept を選びましょう。

エラー3: メインスレッドのブロック。
メインスレッドで get()join() を呼ぶと、非同期の利点を失います。スレッドはタスクの完了を待ってしまい、昔ながらの同期コードと同じになります。ブロックしないチェーンやコールバックを使いましょう。

エラー4: 例外を処理しない。
チェーンで例外が発生しても、exceptionallyhandlewhenComplete といったハンドラーを追加していないと、その例外は「消えて」しまい、見えない失敗で終わることがあります。チェーンでは常にエラー処理を入れましょう。

エラー5: 思わぬ別スレッドでの実行。
非同期メソッド(thenApplyAsync など)は別スレッドで実行されることがあります。スレッドセーフでない変数にアクセスすると、データ競合が発生する可能性があります。

1
タスク
JAVA 25 SELF, レベル 55, レッスン 1
ロック未解除
ゲーム世界のゴールド蓄積:実績の記録
ゲーム世界のゴールド蓄積:実績の記録
1
タスク
JAVA 25 SELF, レベル 55, レッスン 1
ロック未解除
秘密メッセージの解読: 偵察チェーン
秘密メッセージの解読: 偵察チェーン
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION