概要
業務システムでは、外部 API への問い合わせや DB 検索など、複数の非同期処理を組み合わせて結果を返す場面が頻繁にあります。CompletableFuture は Java 8 で導入された非同期プログラミングの基盤ですが、合成メソッドの使い分けや例外処理の設計を誤ると、処理の抜け落ちや無限待ちといった厄介な問題を生みます。この記事では、thenApply・thenCompose・thenCombine の違いを整理したうえで、exceptionally・handle・whenComplete による例外処理パターン、allOf・anyOf を使った複数タスクの合成、そして Java 9 以降の orTimeout・completeOnTimeout によるタイムアウト制御を、実務で使える完結したコードで示します。外部ライブラリなしで、非同期処理の合成から障害対応までを一通り押さえることを目指します。
使いどころ
複数の外部 API(在庫サービス・価格サービス・配送サービス)を並行呼び出しし、結果を合成して注文画面に返す
夜間バッチで数千件のデータを非同期に処理し、全件完了を allOf で待ち合わせてから集計レポートを生成する
UI バックエンドで重い検索処理をバックグラウンドに委譲し、タイムアウト付きで応答を返す
コード例
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class CompletableFuturePatternsDemo {
/** 外部 API 呼び出しを模擬(遅延あり) */
static String fetchInventory(String productId) {
sleep(500);
return "在庫あり: " + productId;
}
static int fetchPrice(String productId) {
sleep(300);
return 2980;
}
static String fetchShipping(String productId) {
sleep(400);
return "翌日配送可";
}
/** タイムアウトするAPI呼び出しを模擬 */
static String fetchSlowApi() {
sleep(5000);
return "遅延レスポンス";
}
static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public static void main(String[] args) {
var productId = "PROD-001";
// --- 1. thenApply: 値の変換 ---
System.out.println("=== thenApply: 値の変換 ===");
var upperResult = CompletableFuture
.supplyAsync(() -> fetchInventory(productId))
.thenApply(String::toUpperCase)
.join();
System.out.println(upperResult);
// --- 2. thenCompose: 非同期処理のチェーン ---
System.out.println("\n=== thenCompose: 非同期チェーン ===");
var composedResult = CompletableFuture
.supplyAsync(() -> fetchInventory(productId))
.thenCompose(inventory -> CompletableFuture
.supplyAsync(() -> inventory + " / 価格: "
+ fetchPrice(productId) + "円"))
.join();
System.out.println(composedResult);
// --- 3. thenCombine: 2つの結果を合成 ---
System.out.println("\n=== thenCombine: 2つの結果合成 ===");
var inventoryFuture = CompletableFuture
.supplyAsync(() -> fetchInventory(productId));
var priceFuture = CompletableFuture
.supplyAsync(() -> fetchPrice(productId));
var combined = inventoryFuture
.thenCombine(priceFuture,
(inv, price) -> inv + " / 価格: " + price + "円")
.join();
System.out.println(combined);
// --- 4. allOf: 複数タスクの待ち合わせ ---
System.out.println("\n=== allOf: 3つのAPIを並行呼び出し ===");
var f1 = CompletableFuture.supplyAsync(
() -> fetchInventory(productId));
var f2 = CompletableFuture.supplyAsync(
() -> String.valueOf(fetchPrice(productId)));
var f3 = CompletableFuture.supplyAsync(
() -> fetchShipping(productId));
CompletableFuture.allOf(f1, f2, f3).join();
System.out.println("在庫: " + f1.join());
System.out.println("価格: " + f2.join() + "円");
System.out.println("配送: " + f3.join());
// --- 5. exceptionally: 例外時のフォールバック ---
System.out.println("\n=== exceptionally: 例外フォールバック ===");
var safeResult = CompletableFuture
.supplyAsync(() -> {
throw new RuntimeException("API接続エラー");
})
.exceptionally(ex -> "デフォルト値(" + ex.getMessage() + ")")
.join();
System.out.println(safeResult);
// --- 6. handle: 正常/例外の両方を処理 ---
System.out.println("\n=== handle: 正常/例外の統一処理 ===");
var handled = CompletableFuture
.supplyAsync(() -> fetchInventory(productId))
.handle((result, ex) -> {
if (ex != null) {
return "エラー: " + ex.getMessage();
}
return "成功: " + result;
})
.join();
System.out.println(handled);
// --- 7. orTimeout: タイムアウト制御(Java 9+) ---
System.out.println("\n=== orTimeout: タイムアウト ===");
try {
var timeoutResult = CompletableFuture
.supplyAsync(() -> fetchSlowApi())
.orTimeout(1, TimeUnit.SECONDS)
.join();
System.out.println(timeoutResult);
} catch (Exception e) {
System.out.println("タイムアウト発生: "
+ e.getCause().getClass().getSimpleName());
}
// --- 8. completeOnTimeout: タイムアウト時にデフォルト値 ---
System.out.println("\n=== completeOnTimeout: デフォルト値 ===");
var fallback = CompletableFuture
.supplyAsync(() -> fetchSlowApi())
.completeOnTimeout("タイムアウト時のデフォルト値",
1, TimeUnit.SECONDS)
.join();
System.out.println(fallback);
}
}Version Coverage
orTimeout / completeOnTimeout(Java 9 追加)が利用可能。var による型推論でチェーン記述が簡潔になる。テキストブロックでログメッセージの整形も容易。
// Java 17: orTimeout で簡潔にタイムアウト設定
var result = CompletableFuture
.supplyAsync(() -> callExternalApi())
.orTimeout(3, TimeUnit.SECONDS)
.exceptionally(ex -> "デフォルト値");
// completeOnTimeout で例外ではなくデフォルト値を返す
var safe = CompletableFuture
.supplyAsync(() -> callExternalApi())
.completeOnTimeout("fallback", 3, TimeUnit.SECONDS);Library Comparison
注意点
thenApply と thenCompose を混同すると CompletableFuture<CompletableFuture<T>> のようにネストする。値を返すなら thenApply、CompletableFuture を返すなら thenCompose を使う
exceptionally で例外を握りつぶすとデフォルト値が後続に流れる。意図しないデフォルト値の伝播がないか、後続のステージで必ず確認する
allOf は CompletableFuture<Void> を返すため、個々の結果は各 future の join() で取得する必要がある。戻り値の型に注意
orTimeout / completeOnTimeout は Java 9 以降でのみ利用可能。Java 8 環境ではスケジューラを使った自前のタイムアウト処理が必要になる
FAQ
変換関数が通常の値を返すなら thenApply、CompletableFuture を返すなら thenCompose を使います。thenApply に CompletableFuture を返す関数を渡すと二重にネストするため注意してください。
例外時のみフォールバック値を返すなら exceptionally で十分です。正常時と例外時の両方で後処理が必要な場合は handle を使います。handle は正常値と例外の両方を引数に受け取ります。
されません。allOf は全 future の完了を待つだけで、1つが例外で完了しても他はそのまま実行されます。失敗時に他をキャンセルしたい場合は、例外ハンドラ内で明示的に cancel() を呼ぶ必要があります。