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

Що таке CompletableFuture і чим він відрізняється від Future

Головна відмінність від звичайного Future:

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

🟢 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"));

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

  1. Забули Async — виконується в тому ж потоці: ```java CompletableFuture.supplyAsync(() -> “Hello”) .thenApply(s -> { // Виконується у ForkJoinPool.commonPool() return s + “ World”; }); // Це НЕ помилка, а свідомий вибір. thenApply в тому ж потоці — // правильний вибір для легких CPU-трансформацій. Проблема тільки якщо // thenApply виконує блокуючу операцію.

// vs CompletableFuture cf = CompletableFuture.completedFuture("Hello"); cf.thenApply(s -> s + " World"); // виконується у потоці, що викликає!


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() і коли його використовувати]]