Які основні переваги CompletableFuture перед Future
CompletableFuture має 5 головних переваг перед звичайним Future:
🟢 Junior Level
CompletableFuture має 5 головних переваг перед звичайним Future:
- Не блокує — можна будувати ланцюжки без очікування
- Комбінування — можна об’єднувати декілька CompletableFuture
- Обробка помилок — вбудовані методи для виключень
- Створення вручну — можна завершити результат у будь-який момент
- Більше методів — 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);
Типові помилки
- Не використовують 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, а в яких - реактивне програмування]]