Вопрос 3 · Раздел 19

Как создать CompletableFuture, который уже завершён с результатом?

Иногда нужно вернуть уже готовый CompletableFuture — например, когда данные есть в кэше или для тестирования.

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

Иногда нужно вернуть уже готовый CompletableFuture — например, когда данные есть в кэше или для тестирования.

completedFuture(value)

// Мгновенно завершённый CF с результатом
CompletableFuture<String> cf = CompletableFuture.completedFuture("Hello");

cf.thenAccept(s -> System.out.println(s));  // Hello — без задержки

failedFuture(Throwable) — Java 9+

// Мгновенно завершённый CF с ошибкой
CompletableFuture<String> cf = CompletableFuture.failedFuture(
    new RuntimeException("Error")
);

cf.exceptionally(ex -> "fallback");  // поймает ошибку

До Java 9 приходилось делать вручную:

CompletableFuture<String> cf = new CompletableFuture<>();
cf.completeExceptionally(new RuntimeException("Error"));

🟡 Middle Level

Зачем это нужно в реальных проектах?

1. Условная асинхронность (Caching):

public CompletableFuture<Data> getData(String id) {
    Data cached = cache.get(id);
    if (cached != null) {
        return CompletableFuture.completedFuture(cached); // Мгновенный ответ
    }
    return CompletableFuture.supplyAsync(() -> db.fetch(id)); // Асинхронный запрос
}

2. Fallback / Null Object:

return CompletableFuture.completedFuture(Collections.emptyList());

3. Мокирование и тестирование:

// В Unit-тестах — синхронный режим
when(service.getDataAsync("1")).thenReturn(
    CompletableFuture.completedFuture(testData)
);

Критический нюанс: Поток выполнения

Если вы вызываете .thenApply() на уже завершенном CF:

  • Коллбэк выполнится в ТЕКУЩЕМ потоке (в том, который вызывает thenApply)
  • Если CF ещё не завершен, коллбэк выполнится в потоке, который завершит CF

Следствие: Если вы ожидали асинхронности, но получили завершенный CF, ваш “тяжелый” коллбэк может заблокировать главный поток.

Решение: Используйте *Async версии методов, если не уверены в источнике CF.

// Безопасно: thenApplyAsync всегда переключит поток
completedFuture.thenApplyAsync(data -> heavyTransform(data), executor);

// Опасно: может выполниться в текущем потоке
completedFuture.thenApply(data -> heavyTransform(data));

completedStage(value) — Java 12+

// Возвращает CompletionStage — нельзя завершить вручную
CompletionStage<String> stage = CompletableFuture.completedStage("Hello");
// stage.complete(...) — не скомпилируется!

Иммутабельное представление — безопаснее для публичных API.


🔴 Senior Level

Производительность и Highload

  • Allocation: completedFuture создает объект в куче. В экстремальных случаях (миллионы запросов в сек) это может нагружать GC.
  • Project Valhalla: В будущем CF может стать Value-типом (или быть оптимизирован через Scalar Replacement), что уберет оверхед на аллокацию. // Project Valhalla — в разработке, timeline не определён. // Не рассчитывайте на это в production-решениях.

Edge Cases

1. Failed Future vs exceptionally: failedFuture сразу переводит цепочку в состояние ошибки. Все последующие thenApply будут пропущены.

2. Short-circuit: Если в середине длинной цепочки стоит exceptionally, который вернул результат, все последующие thenApply будут работать с этим результатом, как будто ошибки и не было.

Best Practices

// ✅ Используйте completedFuture для кэша
if (cached != null) return CompletableFuture.completedFuture(cached);

// ✅ failedFuture для ошибок (Java 9+)
return CompletableFuture.failedFuture(new BusinessException("Not found"));

// ✅ completedStage для публичных API (Java 12+)
public CompletionStage<Data> getData() { ... }

// ❌ Не блокируйте в коллбэках завершённых CF
// ❌ Не игнорируйте что CF уже завершён

🎯 Шпаргалка для интервью

Обязательно знать:

  • completedFuture(value) — мгновенно завершённый CF с результатом
  • failedFuture(Throwable) — Java 9+, мгновенно завершённый CF с ошибкой
  • completedStage(value) — Java 12+, иммутабельное CompletionStage
  • completedFuture вызывается в ТОМ ЖЕ потоке — коллбэки выполняются синхронно
  • completedFuture полезен для кэша, моков, fallback, conditional async

Частые уточняющие вопросы:

  • Зачем completedFuture если результат уже есть? — Для единого async API: кэш возвращает completedFuture, БД — supplyAsync
  • completedFuture.thenApply — в каком потоке выполнится? — В текущем (вызывающем) потоке, т.к. CF уже завершён
  • Как создать failed CF в Java 8? — new CompletableFuture() + completeExceptionally(ex)
  • Когда использовать completedStage вместо completedFuture? — Для публичных API, чтобы caller не мог завершить вручную

Красные флаги (НЕ говорить):

  • «completedFuture создаёт новый поток» — он синхронный, без потоков
  • «completedFuture.thenApplyAsync всегда безопасен» — Async переключит поток, но overhead для лёгких операций избыточен
  • «failedFuture и exceptionally это одно и то же» — failedFuture создаёт CF в состоянии ошибки, exceptionally обрабатывает ошибку существующего

Связанные темы:

  • [[26. Можно ли вручную заверить CompletableFuture результатом]]
  • [[6. Как обрабатывать исключения в цепочке CompletableFuture]]
  • [[16. Что делает метод supplyAsync() и когда его использовать]]
  • [[23. Как тестировать код с CompletableFuture]]