W3docs

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çãoComportamento
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étodoTipo do lambdaRetorna
thenApplyFunction<T,U>CompletableFuture<U>
thenAcceptConsumer<T>CompletableFuture<Void>
thenRunRunnableCompletableFuture<Void>

Cada método possui três variantes:

  • thenApply(fn) — executa na thread que completou o estágio anterior
  • thenApplyAsync(fn) — executa no pool comum
  • thenApplyAsync(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 complete

anyOf 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étodoQuando executaO que retorna
exceptionally(fn)Somente em falha; recebe a causaValor 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.

java— editable, runs on the server

O que observar na execução:

  • A seção 1 usou thenCombine em duas buscas independentes. Elas executaram em paraleloname (50 ms) e age (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 thenCompose para encadear etapas onde cada etapa é assíncrona. thenApply teria gerado CompletableFuture<CompletableFuture<String>> — inútil. thenCompose achata, da mesma forma que flatMap faz para streams e Optional.
  • A seção 3 usou allOf sobre uma lista e depois thenApply para extrair os valores. O allOf em si retorna Void; a coleta dos resultados é um stream separado sobre os futures (agora completos) usando join(). As chamadas join() não bloqueiam aqui porque o allOf já completou.
  • A seção 4 mostrou exceptionally recuperando de uma tarefa que lançou exceção. O future upstream falhou; o future downstream retornou a string de fallback. Sem exceptionally (ou handle), a falha se propagaria para .join() como CompletionException.
  • A seção 5 usou orTimeout para aplicar um prazo de 100 ms a uma tarefa de 500 ms. O future completou excepcionalmente com TimeoutException; o join relançou dentro de CompletionException. Essa é a forma correta para "quero esse resultado, mas somente se aparecer rápido o suficiente."
  • A seção 6 usou handle para ramificar em sucesso/falha em um único passo. handle sempre 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.

Prática

Prática
Você escreve `CompletableFuture.supplyAsync(() -> { Integer x = otherFuture().get(); return x * 2; })`. Dentro do lambda você chama `.get()` em outro future submetido ao mesmo pool padrão. Qual é o risco?
Você escreve `CompletableFuture.supplyAsync(() -> { Integer x = otherFuture().get(); return x * 2; })`. Dentro do lambda você chama `.get()` em outro future submetido ao mesmo pool padrão. Qual é o risco?
Was this page helpful?