はじめに (対象読者・この記事でわかること)

この記事は、Javaの非同期プログラミングにCompletableFutureを使っているが、thenComposeのメソッドチェーンを条件に応じて途中で抜けたいと考えている開発者を対象としています。CompletableFutureはthenComposeを使うことで非同期処理を直列化できますが、「特定の条件を満たしたらそれ以降のチェーンを実行したくない」というケースに直面することがあります。この記事を読むことで、CompletableFutureのメソッドチェーンを条件分岐付きで中断する実装パターンと、エラーハンドリングを組み合わせた安全な中断方法が身につきます。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。

  • Java 8以降のラムダ式とStream APIの基本的な知識
  • CompletableFutureの基本的な使い方(supplyAsync、thenApply、thenComposeなど)

CompletableFutureとthenComposeの概要

CompletableFutureはJava 8で導入された非同期処理を扱うAPIです。thenComposeは「前の非同期処理の結果を受け取って、また新しいCompletableFutureを返す」という形で非同期処理を直列化するメソッドです。たとえば、外部APIへの非同期呼び出しを連鎖させて、最終的な結果を取得するようなケースで威力を発揮します。

しかし、thenComposeを連鎖させていくと「特定の中間結果を見て、それ以上先に進みたくない」という要件が出てきます。従来の同期処理であれば早期returnやbreakで済むところを、CompletableFutureのメソッドチェーンではどうすればよいのでしょうか。

thenComposeのメソッドチェーンを条件分岐で中断する実装

ステップ1:例外送出による中断パターン

最もシンプルでJavaらしい方法は、専用の例外を送出してチェーンを中断する方法です。

Java
class BreakChainException extends RuntimeException { } CompletableFuture<String> future = CompletableFuture .supplyAsync(() -> fetchUserId()) // ① .thenCompose(id -> { if (id == null) { throw new BreakChainException(); // ② 中断 } return fetchUserName(id); // ③ }) .thenCompose(name -> sendGreeting(name)) // ④ .handle((result, throwable) -> { // ⑤ 復帰 if (throwable instanceof BreakChainException) { return "skipped"; } if (throwable != null) { throw new RuntimeException(throwable); } return result; });

ポイントは②で独自例外を送出し、⑤で復帰することで正常系と区別できることです。

ステップ2:Optionalでラップして中断パターン

例外送出が気になる場合は、Optionalでラップして「空」で流す方法もあります。

Java
CompletableFuture<Optional<String>> future = CompletableFuture .supplyAsync(() -> fetchUserId()) .thenCompose(id -> { if (id == null) { return CompletableFuture.completedFuture(Optional.empty()); } return fetchUserName(id) .thenApply(Optional::ofNullable); }) .thenCompose(opt -> opt .map(name -> sendGreeting(name) .thenApply(Optional::ofNullable)) .orElse(CompletableFuture.completedFuture(Optional.empty())));

このパターンは、Optionalで継続/中断を表現します。ただし、型がOptionalでラップされるため、チェーン全体の可読性が若干下がります。

ステップ3:独自の「ショートカット」型を作るパターン

チェーン専用のショートカトークラスを用意して、ハンドラで分岐する方法もあります。

Java
class Shortcut { } CompletableFuture<Object> future = CompletableFuture .supplyAsync(() -> fetchUserId()) .thenCompose(id -> { if (id == null) { return CompletableFuture.completedFuture(new Shortcut()); } return fetchUserName(id); }) .thenCompose(obj -> { if (obj instanceof Shortcut) { return CompletableFuture.completedFuture("early-exit"); } String name = (String) obj; return sendGreeting(name); });

ハマった点とエラー解決

  1. 例外をハンドリングし忘れると完了しない
    handle/exceptionallyを忘れるとBreakChainExceptionが最上位に伝播し、Futureが正常に完了しなくなります。必ず復帰処理を書きましょう。

  2. checked exceptionをそのまま投げるとコンパイルエラー
    thenComposeのラムダ内ではchecked exceptionをそのまま投げられません。ラップするか、独自のRuntimeExceptionを定義してください。

  3. Nullをreturnしてしまうと後続が呼ばれてしまう
    thenCompose(() -> null)とすると、次の段階でNullPointerExceptionが発生します。空のCompletableFutureやOptionalで代替してください。

解決策

上記3パターンのうち、最も推奨できるのは「例外送出+handle」です。理由は以下の通りです。

  • 型安全性が高い(Optionalラップが不要)
  • 意図しない中断を明示的に例外として扱える
  • handleで集約的にログ・監視が可能

より大規模なシステムでは、マイクロソフトの「サーキットブレーカー」パターンを参考に、「失敗が続いたら一定時間中断する」ような実装をCompletableFutureでも応用できます。

まとめ

本記事では、CompletableFutureのthenComposeメソッドチェーンを条件に応じて中断する3つの実装パターンを紹介しました。

  • 例外送出パターン:直感的で型安全。handleで復帰処理が必須
  • Optionalラップパターン:例外を使いたくない場合に有効だが、可読性がやや下がる
  • ショートカット型パターン:チェーン専用のマーカー型を用意して分岐

この記事を通して、非同期処理のフロー制御を安全かつ読みやすく実装するヒントになれば幸いです。
次回は、CompletableFutureで並列実行して最初に完了したものだけを採用する「レース」パターンについて掘り下げます。

参考資料