Питання 2 · Розділ 19

Які основні переваги CompletableFuture перед Future

CompletableFuture має 5 головних переваг перед звичайним Future:

Мовні версії: English Russian Ukrainian

🟢 Junior Level

CompletableFuture має 5 головних переваг перед звичайним Future:

  1. Не блокує — можна будувати ланцюжки без очікування
  2. Комбінування — можна об’єднувати декілька CompletableFuture
  3. Обробка помилок — вбудовані методи для виключень
  4. Створення вручну — можна завершити результат у будь-який момент
  5. Більше методів — 40+ методів для різних сценаріїв
// Future — тільки чекати
Future<String> future = executor.submit(() -> "Hello");
String result = future.get();  // блокує!

// CompletableFuture — ланцюжок без очікування
CompletableFuture.supplyAsync(() -> "Hello")
    .thenApply(s -> s + " World")
    .thenAccept(System.out::println);  // не блокує!

🟡 Middle Level

Детальні переваги

1. Composability (Компонованість):

// Future — не можна комбінувати
Future<Integer> f1 = executor.submit(() -> 10);
Future<Integer> f2 = executor.submit(() -> 20);
// Потрібно чекати обидва і складати вручну

// CompletableFuture — thenCombine
CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> 10);
CompletableFuture<Integer> cf2 = CompletableFuture.supplyAsync(() -> 20);

cf1.thenCombine(cf2, (a, b) -> a + b)
   .thenAccept(System.out::println);  // 30

2. Обробка помилок:

// Future — try/catch вручну
try {
    Integer result = future.get();
} catch (ExecutionException e) {
    // manual handling
}

// CompletableFuture — exceptionally
cf.exceptionally(ex -> {
    log.error("Error", ex);
    return defaultValue;
});

3. Manual completion:

// Можна завершити вручну
CompletableFuture<String> cf = new CompletableFuture<>();

// В іншому потоці:
cf.complete("result");     // встановити результат
cf.completeExceptionally(new RuntimeException("Error"));  // помилку

4. Async chain без блокування:

// Future — блокує на кожному кроці
String url = future1.get();
String data = future2.get();
Result result = process(url, data);

// CompletableFuture — весь ланцюжок асинхронний
supplyAsync(() -> getUrl())
    .thenCompose(url -> fetchDataAsync(url))
    .thenApply(data -> processData(data))
    .thenAccept(result -> save(result));

5. Timeout підтримка:

// Java 9+
cf.orTimeout(5, TimeUnit.SECONDS);
cf.completeOnTimeout(defaultValue, 5, TimeUnit.SECONDS);

Типові помилки

  1. Не використовують async методи: ```java // ❌ Виконується в тому ж потоці cf.thenApply(s -> s.toUpperCase());

// ✅ Виконується асинхронно cf.thenApplyAsync(s -> s.toUpperCase(), executor);

// thenApply в тому ж потоці — це НЕ баг, а intentional design.
// Використовуйте для легких трансформацій. Для blocking — тільки *Async.

---

## 🔴 Senior Level

### Internal Implementation

**CompletionStage interface:**
```java
// CompletableFuture implements CompletionStage<T>
// CompletionStage надає 40+ методів:
// - thenApply, thenAccept, thenRun
// - thenCombine, thenCompose
// - allOf, anyOf
// - exceptionally, handle, whenComplete

// Кожен метод повертає CompletionStage — можна будувати ланцюжки

Non-blocking execution:

// CompletableFuture використовує callback-based підхід
// Замість blocking get() — callback виконується при завершенні

class CompletableFuture<T> {
    volatile Object result;
    volatile Completion stack;  // linked list of callbacks

    // При завершенні — всі callbacks виконуються
    void completeValue(T t) {
        // CAS для встановлення result
        // Потім виконуються всі pending callbacks
    }
}

Архітектурні Trade-offs

Підхід Плюси Мінуси
CompletableFuture Неблокуючий, компонуємий Складніше дебажити
Future + get() Простий Блокуючий
Reactive Streams Backpressure, streaming Складніше
Virtual Threads Простий код, non-blocking Java 21+

Edge Cases

1. Exception wrapping:

// Виключення загортаються в CompletionException
cf.thenApply(s -> {
    throw new RuntimeException("Original");
}).exceptionally(ex -> {
    // ex — CompletionException
    // ex.getCause() — оригінальне виключення
    return null;
});

2. Cancellation:

// CompletableFuture.cancel() не перериває виконання
cf.cancel(true);  // mayInterruptIfRunning ігнорується

// Потрібно обробляти вручну
cf.obtrudeValue(null);  // примусово завершити

Продуктивність

// Порядок величин (залежить від JVM і workload):
// Callback: наносекунди, Async: мікросекунди, Blocking: мілісекунди+

Production Experience

Microservice communication:

@Service
public class OrderService {
    private final UserService userService;
    private final InventoryService inventoryService;
    private final PaymentService paymentService;

    public CompletableFuture<Order> createOrder(CreateOrderRequest req) {
        return userService.validateUserAsync(req.userId())
            .thenCompose(user ->
                inventoryService.checkStockAsync(req.items())
                    .thenCompose(stock ->
                        paymentService.processPaymentAsync(req.payment())
                            .thenApply(payment ->
                                new Order(user, stock, payment)
                            )
                    )
            )
            .exceptionally(ex -> {
                log.error("Order creation failed", ex);
                throw new OrderCreationException(ex);
            });
    }
}

Best Practices

// ✅ Ланцюжки замість блокування
cf.thenApply(...).thenAccept(...);

// ✅ Обробка помилок
cf.exceptionally(ex -> defaultValue);

// ✅ Свій Executor для I/O
CompletableFuture.supplyAsync(task, ioExecutor);

// ✅ Комбінування
cf1.thenCombine(cf2, (a, b) -> combine(a, b));

// ❌ Блокуючий get() без причини
// ❌ Ігнорування виключень
// ❌ commonPool для I/O операцій

Коли НЕ використовувати CompletableFuture

  • Simple fire-and-forget — достатньо executor.submit()
  • Virtual Threads (Java 21+) — часто простіше писати синхронний код
  • Стріми даних — краще reactive (Flux/Mono)

🎯 Шпаргалка для співбесіди

Обов’язково знати:

  • Неблокуюча композиція — ланцюжки без блокування get()
  • Комбінування — thenCombine, allOf, anyOf для паралельних викликів
  • Обробка помилок — exceptionally, handle, whenComplete
  • Manual completion — complete(), completeExceptionally()
  • 40+ методів CompletionStage для будь-яких сценаріїв
  • Timeout підтримка — orTimeout(), completeOnTimeout() (Java 9+)

Часті уточнюючі питання:

  • Головна перевага перед Future? — Неблокуюча композиція та обробка помилок без get()
  • Як комбінувати два CF? — thenCombine(cf2, combiner) для паралельних, thenCompose для залежних
  • Коли НЕ використовувати? — Simple fire-and-forget, Virtual Threads (Java 21+), стріми даних (краще Reactor)
  • Як уникнути блокування? — Ланцюжки thenApply/thenAccept замість get()/join()

Червоні прапорці (НЕ говорити):

  • «Future і CompletableFuture взаємозамінні» — у Future немає ланцюжків і обробки помилок
  • «thenCompose і thenCombine це одне й те саме» — thenCompose послідовний, thenCombine паралельний
  • «Завжди використовую commonPool» — для I/O це thread pool starvation

Пов’язані теми:

  • [[1. Що таке CompletableFuture і чим він відрізняється від Future]]
  • [[4. У чому різниця між thenApply() і thenCompose()]]
  • [[8. Як комбінувати результати декількох CompletableFuture]]
  • [[24. В яких випадках краще використовувати CompletableFuture, а в яких - реактивне програмування]]