Java CompletableFuture
Componha computações assíncronas com CompletableFuture — thenApply, thenCompose, allOf, exceptionally e as armadilhas a evitar.
Future é um identificador de resultado de uso único: você submete, espera e lê. Não permite encadeamento. Se quiser "fazer A, depois com o resultado de A fazer B, combinar B com C e passar para D" sem escrever uma máquina de estados manualmente, você precisa do CompletableFuture — a reformulação do Java 8 da ideia de resultado assíncrono em torno da composição.
CompletableFuture<V> implementa Future<V>, então toda a API antiga ainda está disponível. A parte nova é a API de combinadores: aproximadamente trinta métodos que permitem construir grafos de fluxo de dados de trabalho assíncrono — aplicar funções, executar efeitos colaterais, combinar múltiplos futures, recuperar de exceções, definir timeouts — sem nunca bloquear uma thread para aguardar um resultado intermediário.
Os métodos de inicialização
Geralmente você não constrói um CompletableFuture diretamente. Você inicia um pipeline com um destes:
CompletableFuture<Integer> a = CompletableFuture.supplyAsync(() -> 42);
CompletableFuture<Void> b = CompletableFuture.runAsync(() -> log("hello"));
CompletableFuture<String> c = CompletableFuture.completedFuture("ready");
CompletableFuture<String> d = CompletableFuture.failedFuture(new IOException("nope"));| Método de inicialização | Comportamento |
|---|---|
supplyAsync(Supplier) | Executa um Supplier no pool comum, retorna seu valor |
runAsync(Runnable) | Executa um Runnable no pool comum, sem valor |
completedFuture(v) | Um future já resolvido com o valor fornecido |
failedFuture(t) | Um future já falho com o throwable fornecido |
supplyAsync e runAsync possuem sobrecargas que aceitam um Executor explícito. Você quase sempre vai querer passar um. O padrão é ForkJoinPool.commonPool() — um pool compartilhado dimensionado pelo número de CPUs, adequado para trabalho curto de CPU, mas desastroso para I/O (uma chamada lenta bloqueia um núcleo para todos). Sempre passe um executor explícito para I/O ou trabalho de custo desconhecido.
Encadeamento: thenApply, thenAccept, thenRun
Os combinadores mais simples transformam um future em outro:
CompletableFuture<Integer> a = CompletableFuture.supplyAsync(() -> 42);
CompletableFuture<String> b = a.thenApply(n -> "value is " + n); // transform
CompletableFuture<Void> c = a.thenAccept(n -> System.out.println(n)); // consume, no result
CompletableFuture<Void> d = a.thenRun(() -> System.out.println("done")); // side-effect, ignore value| Método | Tipo do lambda | Retorna |
|---|---|---|
thenApply | Function<T,U> | CompletableFuture<U> |
thenAccept | Consumer<T> | CompletableFuture<Void> |
thenRun | Runnable | CompletableFuture<Void> |
Cada método possui três variantes:
thenApply(fn)— executa na thread que completou o estágio anteriorthenApplyAsync(fn)— executa no pool comumthenApplyAsync(fn, executor)— executa em um executor específico
A forma não-Async é a mais rápida (sem troca de thread), mas significa que sua fn executa em qualquer thread que completou o estágio anterior — possivelmente a thread de I/O que você não quer ocupar com trabalho de CPU. As formas *Async são o padrão mais seguro em pipelines heterogêneos.
thenCompose — achatar um future de um future
thenApply funciona bem quando a função retorna um valor simples. Quando ela retorna outro CompletableFuture, você não quer um CompletableFuture<CompletableFuture<V>> — você quer thenCompose:
CompletableFuture<User> user = lookupUser(id);
CompletableFuture<Profile> profile = user.thenCompose(u -> loadProfile(u.profileId()));
// ^ Function<User, CompletableFuture<Profile>>thenCompose é o flatMap para futures. Use-o sempre que o próximo passo for assíncrono; use thenApply quando não for.
Combinando dois futures: thenCombine
Quando você tem dois valores assíncronos independentes e quer combiná-los:
CompletableFuture<Integer> price = fetchPrice(symbol);
CompletableFuture<Integer> shares = fetchShares(account);
CompletableFuture<Integer> total = price.thenCombine(shares, (p, s) -> p * s);thenCombine aguarda ambas as entradas e aplica uma BiFunction aos seus resultados. Os dois futures são executados em paralelo — price e shares já estão em execução quando thenCombine é registrado. O combinador executa na thread que completar por último.
A versão "qualquer um", applyToEither, pega o primeiro resultado e ignora o segundo.
Muitos futures: allOf e anyOf
Quando o paralelismo é sobre uma coleção de futures:
List<CompletableFuture<String>> all = ids.stream()
.map(this::fetchAsync)
.toList();
CompletableFuture<Void> doneAll = CompletableFuture.allOf(all.toArray(new CompletableFuture[0]));
CompletableFuture<Object> firstOne = CompletableFuture.anyOf(all.toArray(new CompletableFuture[0]));allOf completa quando todas as entradas terminam. Retorna CompletableFuture<Void> — para obter a lista de resultados, você precisa usar thenApply e extraí-los:
CompletableFuture<List<String>> results = doneAll.thenApply(v ->
all.stream().map(CompletableFuture::join).toList()); // .join() never blocks here — they're all completeanyOf retorna o valor de qualquer entrada que completar primeiro (como Object — não há como expressar "qualquer um desses futures tipados" com um único tipo de retorno).
Tratamento de erros: exceptionally e handle
Um CompletableFuture pode falhar (qualquer estágio que lança uma exceção produz um future falho nos estágios seguintes). Os combinadores que recuperam ou transformam:
CompletableFuture<String> safe = riskyAsync()
.exceptionally(ex -> "fallback for: " + ex.getMessage());
CompletableFuture<String> either = riskyAsync()
.handle((value, ex) -> ex == null ? value : "fallback");| Método | Quando executa | O que retorna |
|---|---|---|
exceptionally(fn) | Somente em falha; recebe a causa | Valor recuperado |
handle(bi) | Sempre; recebe (value, ex) (um é null) | Valor transformado |
whenComplete(bi) | Sempre; recebe (value, ex) | Mesmo future, somente efeito colateral |
exceptionally é o caminho simples de "capturar e substituir". handle é o mais geral "sempre executar, decidir com base no resultado" — útil quando você quer registrar cada conclusão independentemente do sucesso.
orTimeout e completeOnTimeout
O Java 9 adicionou timeouts diretamente à API de futures:
CompletableFuture<String> withDeadline = riskyAsync()
.orTimeout(2, TimeUnit.SECONDS); // completes exceptionally if not done in 2s
CompletableFuture<String> withDefault = riskyAsync()
.completeOnTimeout("fallback", 2, TimeUnit.SECONDS);Isso permite expressar prazos sem escrever seu próprio watchdog. Eles usam threads agendadas internas, por isso são baratos de anexar.
Não bloqueie em estágios assíncronos
O maior erro com CompletableFuture: chamar .get() ou .join() dentro de um estágio Async. Isso é uma thread do pool de executores sentada ociosa esperando por outra thread do mesmo pool — sob carga, você pode causar deadlock em todo o pool.
// WRONG — joining inside an async stage on the common pool
CompletableFuture.supplyAsync(() -> {
Integer x = anotherFuture().join(); // blocks a pool thread
return x * 2;
});
// RIGHT — compose instead of join
anotherFuture().thenApply(x -> x * 2);Se você se pegar usando .get() dentro de um estágio Async, o que você queria era thenCompose/thenApply.
Usando seu próprio executor
O pool comum padrão é adequado para trabalho curto de CPU. Para I/O ou qualquer coisa que possa bloquear, use o seu próprio:
ExecutorService io = Executors.newFixedThreadPool(50, namedFactory("io"));
ExecutorService cpu = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), namedFactory("cpu"));
CompletableFuture.supplyAsync(this::loadFromDb, io)
.thenApplyAsync(this::transform, cpu)
.thenAcceptAsync(this::sendToClient, io);Cada etapa é executada no pool correto. O pool comum permanece livre para parallelStream e outros usos do framework. Essa combinação é o coração de um Java assíncrono bem comportado.
Um exemplo prático: um pequeno pipeline assíncrono
O programa abaixo busca um "usuário" e um "perfil" em paralelo, os combina, aplica um prazo e se recupera de um caminho de falha.
O que observar na execução:
- A seção 1 usou
thenCombineem duas buscas independentes. Elas executaram em paralelo —name(50 ms) eage(80 ms) já estavam em execução antes do combinador ser registrado. O future combinado completou logo após o mais lento terminar. Esse é o paralelismo: um pipeline assíncrono não fica aguardando cada etapa — ele compõe as etapas como um grafo. - A seção 2 usou
thenComposepara encadear etapas onde cada etapa é assíncrona.thenApplyteria geradoCompletableFuture<CompletableFuture<String>>— inútil.thenComposeachata, da mesma forma queflatMapfaz para streams eOptional. - A seção 3 usou
allOfsobre uma lista e depoisthenApplypara extrair os valores. OallOfem si retornaVoid; a coleta dos resultados é um stream separado sobre os futures (agora completos) usandojoin(). As chamadasjoin()não bloqueiam aqui porque oallOfjá completou. - A seção 4 mostrou
exceptionallyrecuperando de uma tarefa que lançou exceção. O future upstream falhou; o future downstream retornou a string de fallback. Semexceptionally(ouhandle), a falha se propagaria para.join()comoCompletionException. - A seção 5 usou
orTimeoutpara aplicar um prazo de 100 ms a uma tarefa de 500 ms. O future completou excepcionalmente comTimeoutException; ojoinrelançou dentro deCompletionException. Essa é a forma correta para "quero esse resultado, mas somente se aparecer rápido o suficiente." - A seção 6 usou
handlepara ramificar em sucesso/falha em um único passo.handlesempre executa e recebe ambos(value, ex)— um é null. Útil quando você quer uma cauda uniforme no pipeline independentemente de o trabalho ter sido bem-sucedido.
O que vem a seguir
O próximo capítulo, Java Fork/Join, cobre o ForkJoinPool — o pool de work-stealing que suporta os parallel streams e o pool comum do CompletableFuture, e a ferramenta certa para trabalho de CPU do tipo dividir-e-conquistar.