Питання 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]]