Вопрос 4 · Раздел 19

В чём разница между thenApply() и thenCompose()

Оба метода используются для цепочек CompletableFuture, но отличаются тем, что возвращают:

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

Оба метода используются для цепочек CompletableFuture, но отличаются тем, что возвращают:

  • thenApply() — принимает функцию T -> U, возвращает CompletableFuture<U>
  • thenCompose() — принимает функцию T -> CompletableFuture<U>, возвращает CompletableFuture<U>
// thenApply — простая трансформация
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> "Hello");
cf.thenApply(s -> s.length())     // String -> Integer
  .thenAccept(len -> System.out.println(len));  // 5

// thenCompose — когда функция уже возвращает CompletableFuture
CompletableFuture<String> cf = getUserIdAsync();
cf.thenCompose(userId -> getUserDetailsAsync(userId))  // String -> CompletableFuture<User>
  .thenAccept(user -> System.out.println(user.name()));

Простая аналогия:

  • thenApply — как map в Stream: трансформирует значение
  • thenCompose — как flatMap в Stream: раскрывает вложенный CompletableFuture

🟡 Middle Level

Детальное сравнение

thenApply — для синхронных трансформаций:

CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> "Hello");

// Функция: String -> Integer (синхронная)
cf.thenApply(String::length)       // CompletableFuture<Integer>
  .thenApply(len -> len * 2)       // CompletableFuture<Integer>
  .thenAccept(System.out::println); // 10

thenCompose — для асинхронных операций:

CompletableFuture<String> cf = getUserIdAsync();

// Функция: String -> CompletableFuture<User> (асинхронная)
cf.thenCompose(userId -> getUserAsync(userId))  // CompletableFuture<User>
  .thenCompose(user -> getOrdersAsync(user.id()))  // CompletableFuture<List<Order>>
  .thenAccept(orders -> System.out.println(orders.size()));

Типичные ошибки

  1. Вложенные CompletableFuture: ```java // ❌ thenApply создаёт вложенный CompletableFuture CompletableFuture userId = getUserIdAsync(); CompletableFuture<CompletableFuture> nested = userId.thenApply(id -> getUserAsync(id)); // Nested!

// ✅ thenCompose раскрывает вложенность CompletableFuture flat = userId.thenCompose(id -> getUserAsync(id)); // Flat!


2. **Путаница когда что использовать:**
```java
// thenApply — когда результат синхронный
cf.thenApply(s -> s.toUpperCase());

// thenCompose — когда результат асинхронный
cf.thenCompose(id -> fetchDataFromApi(id));

🔴 Senior Level

Internal Implementation

thenApply:

public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn) {
    return uniApplyStage(null, fn);
}

// Создаёт новый CompletableFuture с UniApply зависимостью
// Когда этот CF завершается — применяется функция fn
// Результат fn устанавливается в новый CF

thenCompose:

public <U> CompletableFuture<U> thenCompose(
    Function<? super T, ? extends CompletionStage<U>> fn
) {
    return uniComposeStage(null, fn);
}

// Создаёт новый CompletableFuture с UniCompose зависимостью
// Когда этот CF завершается — вызывается fn (возвращает CompletionStage)
// Результат fn "раскрывается" в новый CF

Архитектурные Trade-offs

thenApply thenCompose
Синхронная функция Асинхронная функция
T -> U T -> CompletableFuture
Как map Как flatMap
Нет вложенности Раскрывает вложенность

Edge Cases

1. Цепочка thenCompose:

// Типичный async workflow
getUserIdAsync()
    .thenCompose(userId -> getUserAsync(userId))
    .thenCompose(user -> getOrdersAsync(user.id()))
    .thenCompose(orders -> 
        orders.isEmpty() 
            ? CompletableFuture.completedFuture(List.<OrderDetail>of())
            : getOrderDetailsAsync(orders.get(0).id())
    )
    .thenAccept(details -> process(details));

2. Exception propagation:

// Исключение в thenApply:
cf.thenApply(s -> { throw new RuntimeException("Error"); })
  .exceptionally(ex -> "recovered");  // поймает

// Исключение в composed CF:
cf.thenCompose(id -> 
    getUserAsync(id)  // может выбросить
        .exceptionally(ex -> defaultUser)  // обработка внутри
)
.exceptionally(ex -> fallback);  // обработка снаружи

Производительность

thenApply:
- Overhead: ~5 ns (применение функции)
- No thread switch

thenCompose:
- Overhead: ~10 ns (раскрытие CF)
- Может переключить поток (если inner CF async)

thenApplyAsync / thenComposeAsync:
- + ~1μs на переключение потока

Production Experience

Microservice call chain:

@Service
public class OrderFacade {
    public CompletableFuture<OrderSummary> getOrderSummary(Long orderId) {
        return orderRepository.findByIdAsync(orderId)
            .thenCompose(order -> 
                userService.findByIdAsync(order.userId())
                    .thenApply(user -> new OrderSummary(order, user))
            );
    }
    
    // vs с thenApply — если пользователь уже в памяти
    public OrderSummary getOrderSummarySync(Order order, User user) {
        return CompletableFuture.completedFuture(order)
            .thenApply(o -> new OrderSummary(o, user));
    }
}

Best Practices

// ✅ thenApply для синхронных трансформаций
cf.thenApply(s -> s.toUpperCase());

// ✅ thenCompose для асинхронных вызовов
cf.thenCompose(id -> fetchDataAsync(id));

// ✅ Цепочка thenCompose для workflow
cf.thenCompose(a -> callB(a))
  .thenCompose(b -> callC(b));

// ❌ Вложенные CompletableFuture
// ❌ thenCompose когда можно thenApply
// ❌ Игнорирование ошибок в цепочке

Когда НЕ использовать thenCompose

Когда вторая операция НЕ зависит от результата первой и может выполняться параллельно. В этом случае используйте thenCombine для параллельного выполнения.

// ❌ thenCompose — последовательно, unnecessarily slow
CompletableFuture<User> user = getUserAsync(userId);
user.thenCompose(u -> getOrdersAsync(userId));  // не зависит от user!

// ✅ thenCombine — параллельно
CompletableFuture<User> user = getUserAsync(userId);
CompletableFuture<List<Order>> orders = getOrdersAsync(userId);
user.thenCombine(orders, UserProfile::new);

🎯 Шпаргалка для интервью

Обязательно знать:

  • thenApply — функция T -> U, для синхронных трансформаций (как map)
  • thenCompose — функция T -> CompletableFuture, для асинхронных вызовов (как flatMap)
  • thenApply может создать вложенный CompletableFuture<CompletableFuture> — это баг
  • thenCompose раскрывает вложенность, возвращая CompletableFuture
  • thenCompose выполняет последовательно, thenCombine — параллельно

Частые уточняющие вопросы:

  • Когда thenApply, а когда thenCompose? — thenApply если функция возвращает обычное значение, thenCompose если возвращает CompletableFuture
  • Что будет если использовать thenApply с async функцией? — Получится CompletableFuture<CompletableFuture> — вложенность, нужно thenCompose
  • thenCompose vs thenCombine — когда что? — thenCompose когда второй CF зависит от первого, thenCombine когда независимы
  • Какой overhead у thenCompose? — ~10 ns на раскрытие CF, плюс возможное переключение потока

Красные флаги (НЕ говорить):

  • «thenApply и thenCompose взаимозаменяемы» — thenApply создаст вложенный CF
  • «thenCompose быстрее thenCombine» — thenCompose последовательный, thenCombine параллельный
  • «thenApply всегда в том же потоке» — thenApplyAsync переключает поток

Связанные темы:

  • [[11. В чём разница между thenApply() и thenApplyAsync()]]
  • [[22. В чём разница между thenCombine() и thenCompose()]]
  • [[8. Как комбинировать результаты нескольких CompletableFuture]]
  • [[1. Что такое CompletableFuture и чем он отличается от Future]]