Как создать CompletableFuture, который уже завершён с результатом?
Иногда нужно вернуть уже готовый CompletableFuture — например, когда данные есть в кэше или для тестирования.
🟢 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]]