Питання 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]]