No Java, CompletableFuture oferece duas formas principais de continuar uma computação assíncrona: thenApply e thenCompose. Ambas produzem um novo CompletableFuture, mas a forma como tratam o valor intermediário é distinta.
- Conceitos fundamentais
thenApply aplica uma transformação síncrona sobre o resultado de um estágio anterior. A função recebe o valor e devolve um novo valor, que será automaticamente embrulhado em outro CompletableFuture.
public <U> CompletableFuture<U> thenApply(Function<? super T, ? extends U> fn)
thenCompose é o equivalente a um flatMap para CompletionStage: a função recebe o resultado e devolve um novo CompletionStage, que é "achatado" para um único nível de CompletableFuture.
public <U> CompletableFuture<U> thenCompose(Function<? super T, ? extends CompletionStage<U>> fn)
- Comparação
| Característica | thenApply |
thenCompose |
|---|---|---|
| Operação realizada | Transforma o resultado | Encadeia outra operação assíncrona |
| Retorno da função | Um valor U |
Um CompletionStage<U> |
| Estrutura do futuro resultante | Pode gerar CompletableFuture<CompletableFuture<U>> |
Sempre CompletableFuture<U> |
| Uso ideal | Mapear, formatar ou calcular algo simples | Invocar uma nova tarefa assíncrona que depende do resultado anterior |
- Exemplo com
thenApply
Considere um futuro que devolve um preço base. Aplicar um desconto simples é uma transformação direta:
CompletableFuture<Integer> precoBase = CompletableFuture.supplyAsync(() -> 150);
CompletableFuture<Integer> precoComDesconto = precoBase.thenApply(preco -> {
int desconto = preco / 10; // 10% de desconto
return preco - desconto;
});
System.out.println(precoComDesconto.join()); // 135
- Exemplo com
thenCompose
Agora imagine que o frete só pode ser calculado de forma assíncrona, dependendo do preço final. thenCompose é a escolha natural:
CompletableFuture<Integer> precoBase = CompletableFuture.supplyAsync(() -> 150);
CompletableFuture<Integer> precoFinal = precoBase.thenCompose(preco ->
CompletableFuture.supplyAsync(() -> {
int frete = 25;
return preco + frete;
})
);
System.out.println(precoFinal.join()); // 175
- Problema do aninhamento
Se usarmos thenApply para retornar outro CompletableFuture, o tipo resultante será aninhado:
CompletableFuture<CompletableFuture<Integer>> aninhado = precoBase
.thenApply(preco -> CompletableFuture.supplyAsync(() -> preco + 25));
System.out.println(aninhado.join().join()); // 175
Com thenCompose, esse aninhamento desaparece e o resultado permanece em um único nível, tornando o pipeline assíncrono mais simples e seguro:
CompletableFuture<Integer> achatado = precoBase
.thenCompose(preco -> CompletableFuture.supplyAsync(() -> preco + 25));
System.out.println(achatado.join()); // 175
Por isso, prefira thenApply quando a próxima etapa apenas transforma o valor; use thenCompose quando ela precisar iniciar uma nova operação assíncrona.