Criando instâncias básicas do CompletableFuture
O CompletableFuture é uma das ferramentas mais poderosas do Java para programação assíncrona. Vejamos como instanciá-lo manualmente e resolver a promise resultante:
import java.util.Random;
import java.util.concurrent.CompletableFuture;
public class AsyncDemo {
private static final Random gen = new Random();
public static void main(String[] args) {
var future = new CompletableFuture<Double>();
var worker = new Thread(() -> {
double computedValue = simulateSlowCalculation();
future.complete(computedValue);
});
worker.start();
System.out.println("Executando outras tarefas no thread principal...");
future.whenComplete((result, error) -> {
System.out.println("Valor obtido: " + result);
if (error != null) {
error.printStackTrace();
}
});
}
private static double simulateSlowCalculation() {
try {
Thread.sleep(gen.nextInt(3000));
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
return gen.nextDouble();
}
}
Saída esperada:
Executando outras tarefas no thread principal...
Valor obtido: 0.5478210938472156
Utilizando CompletableFuture.supplyAsync
Na prática, raramente criamos um CompletableFuture diretamente. O método estático supplyAsync é a forma recomendada. Porém, há uma armadilha comum: por padrão, o thread executor é um daemon, o que pode impedir a execução dos callbacks:
import java.util.concurrent.CompletableFuture;
public class DaemonPitfall {
public static void main(String[] args) {
// ATENÇÃO: o callback pode não executar pois o thread daemon encerra junto com main
CompletableFuture.supplyAsync(() -> buscarDadoSimulado())
.whenComplete((valor, erro) -> {
System.out.println("resultado = " + valor);
if (erro != null) erro.printStackTrace();
});
System.out.println("main finalizou antes do callback!");
}
static double buscarDadoSimulado() {
try { Thread.sleep(1500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
return Math.random();
}
}
Solução 1: Bloquear com flag manual
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
public class FlagBasedWait {
public static void main(String[] args) throws InterruptedException {
var pronto = new AtomicBoolean(false);
CompletableFuture.supplyAsync(() -> calcularRaiz(256))
.whenComplete((valor, erro) -> {
System.out.println("√256 = " + valor);
if (erro != null) erro.printStackTrace();
pronto.set(true);
});
System.out.println("Aguardando conclusão...");
while (!pronto.get()) {
Thread.sleep(1);
}
}
static double calcularRaiz(double n) {
return Math.sqrt(n);
}
}
Solução 2: Executor com threads não-daemon (recomendado)
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CustomExecutorSolution {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(2, runnable -> {
Thread t = new Thread(runnable);
t.setDaemon(false);
return t;
});
CompletableFuture.supplyAsync(() -> calcularRaiz(256), pool)
.whenComplete((valor, erro) -> {
System.out.println("√256 = " + valor);
if (erro != null) erro.printStackTrace();
});
System.out.println("Thread principal livre para outras tarefas...");
pool.shutdown();
}
static double calcularRaiz(double n) {
return Math.sqrt(n);
}
}
Transformando resultados com thenApply
O thenApply permite encadear transformações sobre o valor produzido pela etapa anterior, similar ao map de streams:
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TransformExample {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(2, runnable -> {
Thread t = new Thread(runnable);
t.setDaemon(false);
return t;
});
CompletableFuture.supplyAsync(() -> gerarNumeroAleatorio(), pool)
.thenApply(numero -> numero * 100)
.whenComplete((resultado, erro) -> System.out.println("Valor escalado: " + resultado));
pool.shutdown();
}
private static double gerarNumeroAleatorio() {
try { Thread.sleep(800); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
return Math.random() * 10;
}
}
Saída:
Valor escalado: 432.87165432109876
Agregando múltiplos resultados com join()
Uma aplicação clássica é executar várias tarefas em paralelo e coletar seus resultados. O método join() bloqueia a execução até que o CompletableFuture seja concluído:
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
public class ParallelAggregation {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(4, runnable -> {
Thread t = new Thread(runnable);
t.setDaemon(false);
return t;
});
List<Integer> itemIds = List.of(101, 102, 103, 104, 105);
List<Double> precosFinais = itemIds.stream()
.map(id -> CompletableFuture.supplyAsync(() -> consultarPreco(id), pool))
.map(fut -> fut.thenApply(preco -> preco * 1.1)) // aplica 10% de imposto
.map(futComImposto -> futComImposto.join())
.collect(Collectors.toList());
System.out.println("Preços finais: " + precosFinais);
pool.shutdown();
}
static double consultarPreco(int itemId) {
try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
return itemId * 9.99;
}
}
Saída:
Preços finais: [1109.889, 1130.868, 1151.847, 1172.826, 1193.805]
Entendendo o join() em profundidade
O join() é o método central para obter resultados de operações assíncronas. Suas características incluem:
- Bloqueio do thread atual até a conclusão da tarefa
- Lança
CompletionException(unchecked) em caso de falha - Não responde a interrupções de thread
- Extremamente conveniente dentro de expressões lambda
Comparação com get()
| Aspecto | join() | get() |
|---|---|---|
| Exceções | CompletionException (unchecked) | ExecutionException / InterruptedException (checked) |
| Bloco try-catch | Opcional | Obrigatório |
| Interrupção | Ignora Thread.interrupt() | Responde a Thread.interrupt() |
| Legibilidade | Mais conciso | Verboso |
Uso comanyOf e allOf
List<CompletableFuture<String>> futures = List.of(
buscarUsuarioAsync(),
buscarPedidoAsync(),
buscarProdutoAsync()
);
// Aguarda todas as tarefas finalizarem
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.join();
// Coleta os resultados individuais
List<String> resultados = futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
A chamada allOf().join() garante que todas as tarefas foram concluídas antes de prosseguir. Em seguida, cada join() individual retorna imediatamente, pois o resultado já está disponível.
Cenários onde allOf().join() é essencial
// Quando você precisa executar ações após TODAS as tarefas concluírem
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
repositorio.commitTransacao();
notificador.enviarAlerta(resultados);
Quando basta usar join() diretamente
// Para poucas tarefas simples
List<String> dados = List.of(
buscarUsuario().join(),
buscarConfig().join(),
buscarPermissoes().join()
);
Diferenças entre whenComplete e handle
O handle funciona de forma semelhante ao whenComplete, porém permite transformar o valor ou fornecer um fallback em caso de erro:
CompletableFuture.supplyAsync(() -> dividir(10, 0))
.handle((resultado, erro) -> {
if (erro != null) return 0.0;
return resultado;
})
.whenComplete((v, e) -> System.out.println("Valor seguro: " + v));
static double dividir(double a, double b) {
return a / b;
}
Encadeamento com thenCompose
O thenCompose permite que o resultado de uma operação assíncrona alimente outra operação assíncrona, evitando CompletableFuture<CompletableFuture<T>>:
CompletableFuture.supplyAsync(() -> localizarUsuario("joao"))
.thenCompose(usuario -> CompletableFuture.supplyAsync(() -> buscarPerfil(usuario.id())))
.thenAccept(perfil -> System.out.println("Perfil: " + perfil));
Combinando dois futures com thenCombine
O thenCombine executa duas operações independentes em paralelo e funde seus resultados com uma função binária:
CompletableFuture<String> nomeFuture = CompletableFuture.supplyAsync(() -> "Maria");
CompletableFuture<Integer> idadeFuture = CompletableFuture.supplyAsync(() -> 30);
nomeFuture.thenCombine(idadeFuture, (nome, idade) -> nome + " (" + idade + " anos)")
.thenAccept(System.out::println);
// Saída: Maria (30 anos)
Escolhendo o primeiro resultado com applyToEither
Quando deseja utilizar o resultado da primeira tarefa a finalizar:
var servidorA = CompletableFuture.supplyAsync(() -> {
Thread.sleep(900);
System.out.println("Servidor A respondeu");
return "Resposta-A";
});
var servidorB = CompletableFuture.supplyAsync(() -> {
Thread.sleep(50);
System.out.println("Servidor B respondeu");
return "Resposta-B";
});
servidorA.applyToEither(servidorB, resposta -> {
System.out.println("Usando: " + resposta);
return resposta.toUpperCase();
}).thenAccept(System.out::println);
Saída:
Servidor B respondeu
Usando: Resposta-B
RESPOSTA-B
Servidor A respondeu
Aguardando todos ou qualquer um com allOf e anyOf
import java.util.List;
import java.util.concurrent.CompletableFuture;
public class CombinacaoExample {
public static void main(String[] args) throws InterruptedException {
var tarefas = List.of(1, 2, 3, 4).stream()
.map(i -> CompletableFuture.supplyAsync(() -> processarItem(i)))
.toList();
// anyOf: conclui assim que pelo menos UMA finalizar
CompletableFuture.anyOf(tarefas.toArray(new CompletableFuture[0]))
.thenRun(() -> System.out.println("Pelo menos uma tarefa concluída!"));
Thread.currentThread().join();
}
static double processarItem(int id) {
try { Thread.sleep((long)(Math.random() * 3000)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
double val = Math.random();
System.out.println("Item " + id + " processado: " + val);
return val;
}
}
Melhores práticas e armadilhas comuns
Evite chamar join() dentro de threads do pool: isso pode causar deadlocks, pois bloqueia um thread que poderia estar executando outra tarefa.
// ERRADO: pode esgotar o pool
CompletableFuture.supplyAsync(() -> {
var inner = outraTarefaAsync();
return inner.join(); // bloqueia thread do pool!
});
// CORRETO: use thenCompose para encadear
CompletableFuture.supplyAsync(() -> "valor-inicial")
.thenCompose(valor -> outraTarefaAsync(valor));
Controle de tempo (Java 9+):
CompletableFuture<String> future = buscarDadosAsync()
.orTimeout(5, TimeUnit.SECONDS);
try {
String resultado = future.join();
} catch (CompletionException e) {
if (e.getCause() instanceof TimeoutException) {
System.out.println("Tempo limite excedido!");
}
}
Tratamento de exceções com exceptionally:
CompletableFuture<String> seguro = buscarDadosAsync()
.exceptionally(ex -> {
System.err.println("Falha: " + ex.getMessage());
return "valor-padrao";
});
String resultado = seguro.join(); // nunca lança exceção
Resumo dos principais métodos
| Método | Finalidade |
|---|---|
| supplyAsync | Inicia uma computação assíncrona que retorna um valor |
| thenApply | Transforma o resultado (síncrono) |
| thenCompose | Encadeia um novo CompletableFuture (flatMap assíncrono) |
| thenCombine | Dois futures em paralelo, fundidos por uma BiFunction |
| thenAccept | Consome o resultado sem retornar nada |
| thenRun | Executa Runnable após conclusão, sem acesso ao resultado |
| handle | Trata resultado e exceção simultaneamente |
| whenComplete | Observa resultado/erro sem transformá-lo |
| applyToEither | Usa resultado do primeiro future a completar |
| acceptEither | Consome resultado do primeiro a completar |
| thenAcceptBoth | Consome resultados de ambos os futures |
| runAfterBoth | Executa após ambos completarem |
| runAfterEither | Executa após qualquer um completar |
| allOf | Aguarda todos os futures completarem |
| anyOf | Completa quando qualquer um dos futures finalizar |
O princípio fundamental: priorize operações não-bloqueantes como thenApply, thenCompose e thenCombine. Reserve join() para momentos em que realmente precisa sincronizar a execução, como no fluxo principal da aplicação ou em testes.