1. thenCompose vs. thenApply: diferença e quando usar cada um
Na programação assíncrona em Java (com CompletableFuture), muitas vezes é necessário executar cadeias de ações. Para isso, há dois métodos parecidos: thenApply e thenCompose. Mas eles funcionam de maneiras diferentes!
thenApply
O método thenApply é usado quando o próximo passo é uma transformação simples do valor, sem iniciar novas operações assíncronas. Ele recebe o resultado da etapa anterior, o processa e retorna um novo valor (não um CompletableFuture).
Se você conhece a Stream API, então thenApply se comporta aproximadamente como map: pega o resultado, aplica uma função e retorna a versão transformada.
Exemplo:
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> "42");
CompletableFuture<Integer> lengthFuture = cf.thenApply(s -> s.length());
// lengthFuture contém 2 (tamanho da string "42")
Em outras palavras, thenApply é uma forma de dizer: “Quando o resultado estiver pronto, faça isto com ele”.
thenCompose
- Usado quando o próximo passo é outra operação assíncrona (retorna CompletableFuture).
- Permite “desembrulhar” CompletableFuture aninhado (análogo a flatMap).
- Se você usar thenApply com uma função assíncrona, obterá CompletableFuture<CompletableFuture<T>> — nada prático!
Exemplo:
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> "user42");
// Suponha que precisemos obter os pedidos pelo nome de usuário (assíncrono)
CompletableFuture<List<Order>> ordersFuture = cf.thenCompose(username -> fetchOrdersAsync(username));
// fetchOrdersAsync retorna CompletableFuture<List<Order>>
Visualmente:
- thenApply: CF<String> → thenApply(s -> s.length()) → CF<Integer>
- thenCompose: CF<User> → thenCompose(u -> fetchOrdersAsync(u.id)) → CF<List<Order>>
Quando usar o quê?
- A função retorna um valor comum — use thenApply.
- A função retorna um CompletableFuture — use thenCompose.
Exemplo de erro:
cf.thenApply(username -> fetchOrdersAsync(username)); // Você obterá CF<CF<List<Order>>>
cf.thenCompose(username -> fetchOrdersAsync(username)); // Você obterá CF<List<Order>>
2. Gerenciamento de pool de threads (Executor): por que e como usar seu próprio Executor
Padrão: ForkJoinPool.commonPool()
Quando você escreve CompletableFuture.supplyAsync(...) ou thenApplyAsync(...) sem informar um Executor, o Java usa o pool de threads compartilhado — ForkJoinPool.commonPool(). Isso é conveniente, mas nem sempre adequado:
- Se você tiver muitas operações longas ou bloqueantes (requisições de rede, trabalho com arquivos), o pool compartilhado pode ficar saturado e todas as tarefas vão esperar.
- Às vezes, é necessário isolar tarefas com prioridades diferentes ou limitar a quantidade de threads executando simultaneamente.
Quando é necessário um Executor próprio?
- Operações longas e bloqueantes (por exemplo, consultas ao banco, requisições HTTP, leitura de arquivos).
- Isolamento de tarefas: para que tarefas do usuário não atrapalhem tarefas do sistema.
- Limitação de recursos: por exemplo, não iniciar mais que 10 downloads simultâneos.
Como criar seu próprio Executor
Geralmente usa-se ThreadPoolExecutor ou fábricas de Executors:
ExecutorService myExecutor = Executors.newFixedThreadPool(10);
Como usar seu Executor com CompletableFuture
- Nos métodos supplyAsync, runAsync, thenApplyAsync, thenComposeAsync e outros, é possível passar um segundo argumento — o seu Executor.
Exemplos:
CompletableFuture<String> cf = CompletableFuture.supplyAsync(
() -> loadDataFromNetwork(), myExecutor
);
cf.thenApplyAsync(data -> processData(data), myExecutor)
.thenAcceptAsync(result -> System.out.println(result), myExecutor);
Importante: se você não informar um Executor, será usado o ForkJoinPool.commonPool().
Quando o Executor padrão é suficiente?
- Para tarefas curtas, CPU-bound (cálculos simples).
- Quando não importa em qual thread a tarefa será executada.
3. Tratamento de timeouts: orTimeout e completeOnTimeout
Operações assíncronas podem travar ou demorar demais (por exemplo, se o servidor não responder). Para não esperar para sempre, no CompletableFuture existem métodos para trabalhar com timeouts.
orTimeout
- Finaliza o CompletableFuture com a exceção TimeoutException se a operação não terminar no tempo especificado.
- Não cancela a tarefa que está em execução, mas a cadeia subsequente receberá o erro.
Sintaxe:
cf.orTimeout(3, TimeUnit.SECONDS)
.exceptionally(ex -> {
System.out.println("Tempo limite: " + ex);
return null;
});
Exemplo:
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
Thread.sleep(5000); // simulamos uma operação demorada
return "OK";
});
cf.orTimeout(2, TimeUnit.SECONDS)
.exceptionally(ex -> {
System.out.println("Erro: " + ex);
return "TIMEOUT";
});
Resultado:
Após 2 segundos será lançada TimeoutException, e exceptionally tratará o erro.
completeOnTimeout
- Finaliza o CompletableFuture com o valor especificado, se a operação não terminar dentro do tempo limite.
- Não lança exceção; retorna um valor “de reserva”.
Sintaxe:
cf.completeOnTimeout("DEFAULT", 2, TimeUnit.SECONDS);
Exemplo:
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
Thread.sleep(5000);
return "OK";
});
cf.completeOnTimeout("TIMEOUT", 2, TimeUnit.SECONDS)
.thenAccept(System.out::println); // Em 2 segundos imprimirá "TIMEOUT"
Comparação entre orTimeout e completeOnTimeout
| Método | O que faz no timeout? | Como é tratado depois? |
|---|---|---|
|
Finaliza com TimeoutException | Pode ser tratado via exceptionally/handle |
|
Finaliza com o valor especificado | thenAccept/thenApply receberá esse valor |
4. Prática: exemplo com thenCompose, Executor personalizado e timeout
Tarefa:
- Obter o usuário por id (assíncrono, com atraso).
- Em seguida, obter assíncronamente a lista de pedidos do usuário (também com atraso).
- Usar um Executor personalizado.
- Adicionar timeout na obtenção dos pedidos.
import java.util.concurrent.*;
import java.util.*;
public class AsyncDemo {
static ExecutorService ioExecutor = Executors.newFixedThreadPool(4);
// Simulação de obtenção assíncrona do usuário
static CompletableFuture<String> fetchUserAsync(int userId) {
return CompletableFuture.supplyAsync(() -> {
sleep(1000);
return "user" + userId;
}, ioExecutor);
}
// Simulação de obtenção assíncrona dos pedidos do usuário
static CompletableFuture<List<String>> fetchOrdersAsync(String username) {
return CompletableFuture.supplyAsync(() -> {
sleep(3000); // Operação demorada!
return List.of("order1", "order2");
}, ioExecutor);
}
static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException ignored) {}
}
public static void main(String[] args) {
fetchUserAsync(42)
.thenCompose(username ->
fetchOrdersAsync(username)
.orTimeout(2, TimeUnit.SECONDS) // Timeout ao obter os pedidos
.exceptionally(ex -> {
System.out.println("Não foi possível obter os pedidos: " + ex);
return List.of();
})
)
.thenAccept(orders -> System.out.println("Pedidos: " + orders))
.join(); // Aguardamos a conclusão de toda a cadeia
ioExecutor.shutdown();
}
}
O que acontece:
- Obtemos o usuário (1 segundo).
- Obtemos os pedidos (3 segundos, mas o timeout é de 2 segundos).
- Se não der tempo — capturamos TimeoutException e retornamos uma lista vazia.
- Tudo roda em um Executor personalizado.
Resultado:
Não foi possível obter os pedidos: java.util.concurrent.TimeoutException
Pedidos: []
Se você reduzir o atraso em fetchOrdersAsync para 1_000 ms — verá os pedidos reais.
5. Erros comuns e nuances
Erro nº 1: usar thenApply em vez de thenCompose para operações assíncronas.
Se a função retorna um CompletableFuture, mas você aplicou thenApply, terá o tipo aninhado CompletableFuture<CompletableFuture<T>>. Isso complica a cadeia e leva a wrappers desnecessários. Solução: use thenCompose para “achatar” o resultado em CompletableFuture<T>.
Erro nº 2: executar tarefas longas ou de E/S sem um Executor próprio.
Por padrão, as tarefas são executadas em ForkJoinPool.commonPool(). Se ele ficar sobrecarregado, as latências começam a crescer e outras tarefas do aplicativo podem desacelerar. Solução: crie seu próprio ExecutorService e passe-o para supplyAsync/thenApplyAsync.
Erro nº 3: esperar que orTimeout cancele a execução da tarefa.
orTimeout apenas finaliza o CompletableFuture com exceção por timeout, mas a tarefa continua em segundo plano. Solução: se precisar interromper a execução, use cancel(true) ou mecanismos próprios de interrupção.
Erro nº 4: entender incorretamente o escopo do timeout.
orTimeout e completeOnTimeout funcionam apenas para uma etapa específica da cadeia, não para a cadeia toda. Solução: se precisar de um timeout geral para toda a cadeia, envolva-a em um CompletableFuture separado e aplique o timeout a ele.
Erro nº 5: não fechar o ExecutorService.
Se após executar as tarefas você não chamar shutdown()/shutdownNow() no ExecutorService, as threads continuarão ativas e o programa pode ficar bloqueado. Solução: sempre feche o ExecutorService em finally ou use try-with-resources no Java 21+.
GO TO FULL VERSION