Вопрос 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() и когда его использовать]]