Що таке CompletableFuture і чим він відрізняється від Future
Головна відмінність від звичайного Future:
🟢 Junior Level
CompletableFuture — це клас у Java (з’явився у Java 8), який представляє собою результат асинхронної операції, який ще не готовий, але буде готовий у майбутньому.
Головна відмінність від звичайного Future:
Future— тільки очікування результату (get()блокує потік)CompletableFuture— можна будувати ланцюжки дій, комбінувати, обробляти помилки
// Future — потрібно чекати і блокувати потік
Future<String> future = executor.submit(() -> "Hello");
String result = future.get(); // блокує потік!
// CompletableFuture — можна побудувати ланцюжок
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> "Hello");
cf.thenApply(s -> s + " World")
.thenAccept(System.out::println); // не блокує!
Проста аналогія:
Future— як чек у ресторані: чекаєте, поки їжа буде готоваCompletableFuture— як доставка: замовили, і вам привезуть, а ви займаєтеся своїми справами
🟡 Middle Level
Як це працює
Обмеження Future:
// ❌ Future — не можна побудувати ланцюжок
Future<Integer> future = executor.submit(() -> 42);
// Потрібно блокувати і обробляти вручну
try {
Integer result = future.get(); // блокує
Integer doubled = result * 2;
} catch (InterruptedException | ExecutionException e) {
// ручна обробка
}
Можливості CompletableFuture:
// ✅ Ланцюжок дій
CompletableFuture<Integer> cf = CompletableFuture.supplyAsync(() -> 42);
cf.thenApply(result -> result * 2) // трансформація
.thenAccept(doubled -> System.out.println(doubled)) // споживання
.exceptionally(ex -> { // обробка помилок
System.err.println("Error: " + ex);
return null;
});
// Не блокує основний потік!
Створення:
// supplyAsync — з поверненням значення
CompletableFuture<String> cf1 =
CompletableFuture.supplyAsync(() -> "Hello");
// runAsync — без повернення
CompletableFuture<Void> cf2 =
CompletableFuture.runAsync(() -> System.out.println("Done"));
Типові помилки
- Забули Async — виконується в тому ж потоці: ```java CompletableFuture.supplyAsync(() -> “Hello”) .thenApply(s -> { // Виконується у ForkJoinPool.commonPool() return s + “ World”; }); // Це НЕ помилка, а свідомий вибір. thenApply в тому ж потоці — // правильний вибір для легких CPU-трансформацій. Проблема тільки якщо // thenApply виконує блокуючу операцію.
// vs
CompletableFuture
2. **Не обробили виключення:**
```java
// ❌ Виключення втрачено
CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("Error");
}).thenApply(s -> s.toUpperCase());
// ✅ Обробка
CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("Error");
}).exceptionally(ex -> "default");
🔴 Senior Level
Internal Implementation
CompletableFuture vs Future:
// Future — interface
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit) throws ...;
}
// CompletableFuture — реалізація + CompletionStage
public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
// CompletionStage надає 40+ методів для композиції
}
Internal structure:
// CompletableFuture використовує lock-free алгоритм
// CAS (Compare-And-Swap) для завершення
// Stack of completions/dependencies для ланцюжків
class CompletableFuture<T> {
volatile Object result; // або значення, або AltResult (exception)
// AltResult — внутрішній JDK-клас-обгортка для виключень.
// Дозволяє зберігати null як валідний результат і відрізняти його від виключення.
volatile Completion stack; // стек залежностей
// CAS для завершення
boolean completeValue(T t) {
// compareAndSwap для встановлення result
}
}
Архітектурні Trade-offs
| Future | CompletableFuture |
|---|---|
| Блокуючий get() | Неблокуючий |
| Немає обробки помилок | Повна обробка |
| Не можна комбінувати | thenCombine, allOf, anyOf |
| ExecutorService потрібен | Свій ForkJoinPool за замовчуванням |
Edge Cases
1. Default Executor:
// CompletableFuture використовує ForkJoinPool.commonPool()
// Для I/O операцій — погано (мало потоків)
CompletableFuture.supplyAsync(() -> {
// I/O операція — потрібно більше потоків!
return httpClient.get(url);
});
// ✅ Свій Executor
ExecutorService ioExecutor = Executors.newFixedThreadPool(20);
CompletableFuture.supplyAsync(() -> httpClient.get(url), ioExecutor);
2. Exception propagation:
// Виключення загортається в CompletionException
CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("Original");
}).thenApply(s -> s.toUpperCase())
.exceptionally(ex -> {
// ex — CompletionException з оригінальною причиною
Throwable cause = ex.getCause(); // "Original"
return null;
});
Продуктивність
Операція | Future | CompletableFuture
----------------------|--------|------------------
Створення | 5 ns | 10 ns
get() (блокуючий) | 100μs+ | 100μs+
thenApply (ланцюжок) | N/A | 5-10 ns
Обробка помилок | Manual | Вбудована
ForkJoinPool.commonPool():
- Розмір = availableProcessors - 1
- Для CPU-bound задач — OK
- Для I/O — потрібен свій Executor
Production Experience
Async API call:
@Service
public class UserService {
private final RestTemplate restTemplate;
private final ExecutorService executor;
public CompletableFuture<User> getUserAsync(Long userId) {
return CompletableFuture.supplyAsync(() -> {
return restTemplate.getForObject(
"http://api/users/" + userId, User.class
);
}, executor);
}
// Комбінування декількох викликів
public CompletableFuture<UserProfile> getProfile(Long userId) {
CompletableFuture<User> user = getUserAsync(userId);
CompletableFuture<Order> lastOrder = getLastOrderAsync(userId);
return user.thenCombine(lastOrder, (u, o) ->
new UserProfile(u, o)
);
}
}
Best Practices
// ✅ Завжди обробляйте виключення
cf.exceptionally(ex -> defaultValue);
// ✅ Використовуйте свій Executor для I/O
CompletableFuture.supplyAsync(task, ioExecutor);
// ✅ Не блокуйте без необхідності
// ❌ cf.get() — тільки якщо дійсно потрібно
// ❌ Не ігноруйте CompletableFuture
// ❌ Не використовуйте commonPool для I/O
🎯 Шпаргалка для співбесіди
Обов’язково знати:
- CompletableFuture з’явився у Java 8, реалізує Future + CompletionStage
- CompletionStage надає 40+ методів для композиції
- За замовчуванням використовується ForkJoinPool.commonPool() (availableProcessors - 1)
- supplyAsync() — з поверненням значення, runAsync() — без
- Виключення загортаються в CompletionException
- thenApply/thenAccept/thenRun — для ланцюжків, thenCombine/allOf/anyOf — для комбінування
Часті уточнюючі питання:
- Чим відрізняється від Future? — Future тільки блокуючий get(), CompletableFuture — неблокуючий з ланцюжками та обробкою помилок
- Який пул за замовчуванням? — ForkJoinPool.commonPool(), для I/O потрібен свій Executor
- Як обробити помилку? — exceptionally(), handle(), whenComplete()
- Чи можна завершити вручну? — Так, complete() або completeExceptionally()
Червоні прапорці (НЕ говорити):
- «CompletableFuture блокує потік» — він неблокуючий за дизайном
- «Використовую commonPool для HTTP запитів» — thread pool starvation
- «Ігнорую CompletableFuture не обробляючи» — втрачене виключення
Пов’язані теми:
- [[2. Які основні переваги CompletableFuture перед Future]]
- [[12. Який пул потоків використовується за замовчуванням для async методів]]
- [[6. Як обробляти виключення в ланцюжку CompletableFuture]]
- [[16. Що робить метод supplyAsync() і коли його використовувати]]