CompletableFuture e Promises: Processamento de Resultados Assíncronos em Java
O objeto Future representa uma tarefa assíncrona e permite obter seu resultado com o método get().
Contudo, get() bloqueia a thread atual até que o resultado esteja disponível.
O CompletableFuture, que implementa Future, introduz uma abordagem diferente: ele permite reagir ao resultado da tarefa assim que ele é produzido, por meio de callbacks (funções de retorno).
Além disso, CompletableFuture implementa a interface CompletionStage, que representa uma etapa de computação assíncrona.
Essa combinação possibilita criar fluxos de processamento semelhantes aos promises de linguagens como JavaScript — ou seja, operações que podem ser encadeadas e executadas de forma não bloqueante.
Criação de um CompletableFuture
Um CompletableFuture pode ser criado diretamente com o construtor:
CompletableFuture<Integer> future = new CompletableFuture<>();No entanto, é mais comum criá-lo já associado a uma tarefa.
Para isso, o método estático supplyAsync() é o mais usado:
static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)O parâmetro supplier é uma função que fornece o valor a ser processado.
A primeira versão executa a tarefa no pool comum (ForkJoinPool.commonPool()), enquanto a segunda permite especificar um executor personalizado.
Exemplo: cálculo de fatorial
int number = 5;
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
if (number < 1) throw new RuntimeException("Number must be greater than 0");
int n = 1;
int result = 1;
while (n <= number) result *= n++;
return result;
});Aqui, a expressão lambda enviada para supplyAsync() calcula o fatorial do número.
Essa função é executada de forma assíncrona e retorna um valor que será processado posteriormente.
O tipo Supplier não pode lançar exceções verificadas, portanto, utiliza-se RuntimeException em caso de erro.
Adicionando um callback
Depois de criado o CompletableFuture, é possível registrar uma função que será chamada assim que o resultado estiver disponível.
Entre os métodos disponíveis, destacam-se:
thenAccept(Consumer<? super T> action)— consome o resultado e não retorna nada.thenApply(Function<? super T, ? extends U> fn)— aplica uma função e retorna um novoCompletableFuturecom o valor transformado.thenRun(Runnable action)— executa uma ação independente do resultado.thenCompose(Function<? super T, ? extends CompletionStage<U>> fn)— encadeia outra computação assíncrona.
O método thenAccept() é uma forma simples de lidar com o resultado:
import java.util.concurrent.*;
import java.util.function.Supplier;
class Program {
public static void main(String[] args) throws Exception {
System.out.println("Main thread started...");
int number = 5;
Supplier<Integer> task = () -> {
if (number < 1) throw new RuntimeException("Number must be greater than 0");
int n = 1;
int result = 1;
while (n <= number) result *= n++;
return result;
};
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(task);
future.thenAccept(result ->
System.out.printf("Factorial of %d is %d\n", number, result));
System.out.println("Main thread works...");
Thread.sleep(2000);
System.out.println("Main thread finished...");
}
}Saída esperada:
Main thread started... Main thread works... Factorial of 5 is 120 Main thread finished...
Nesse exemplo:
- A tarefa de cálculo do fatorial é executada de forma assíncrona.
- O callback em
thenAccept()é chamado automaticamente quando o resultado está disponível. - O
maincontinua executando enquanto a tarefa roda em paralelo.
Observação sobre threads daemon
As threads usadas por padrão em CompletableFuture vêm do ForkJoinPool.commonPool(), que cria threads daemon — ou seja, threads em segundo plano.
A JVM encerra a execução assim que todas as threads não daemon terminam.
Por isso, se o main finalizar rapidamente, o callback pode não ser executado.
No exemplo acima, a chamada Thread.sleep(2000) mantém a thread principal ativa por tempo suficiente para que o callback termine sua execução.
Executor personalizado
Em vez de depender do pool padrão, é possível criar um executor dedicado para gerenciar as threads:
import java.util.concurrent.*;
import java.util.function.Supplier;
class Program {
public static void main(String[] args) throws Exception {
System.out.println("Main thread started...");
int number = 5;
Supplier<Integer> task = () -> {
if (number < 1) throw new RuntimeException("Number must be greater than 0");
int n = 1;
int result = 1;
while (n <= number) result *= n++;
try { Thread.sleep(2000); } catch (InterruptedException _) {}
return result;
};
ExecutorService executor = Executors.newCachedThreadPool();
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(task, executor);
future.thenAccept(result ->
System.out.printf("Factorial of %d is %d\n", number, result));
System.out.println("Main thread works...");
System.out.println("Main thread finished...");
executor.close();
}
}Saída provável:
Main thread started... Main thread works... Main thread finished... Factorial of 5 is 120
Aqui, o executor é responsável tanto pela execução da tarefa quanto pelo callback. Como a tarefa inclui uma pausa de 2 segundos, o resultado aparece após o encerramento da thread principal.
Encadeamento de callbacks
Os métodos do CompletableFuture permitem formar cadeias de processamento.
Por exemplo, com thenApply() é possível transformar o resultado antes de exibi-lo:
import java.util.concurrent.*;
import java.util.function.Supplier;
import java.util.function.Function;
import java.util.function.Consumer;
class Program {
public static void main(String[] args) throws Exception {
System.out.println("Main thread started...");
int number = 5;
Supplier<Integer> factorialTask = () -> {
if (number < 1) throw new RuntimeException("Number must be greater than 0");
int n = 1;
int result = 1;
while (n <= number) result *= n++;
return result;
};
Function<Integer, Integer> doubleTask = result -> result * 2;
Consumer<Integer> printTask = result ->
System.out.printf("Final result: %d\n", result);
ExecutorService executor = Executors.newCachedThreadPool();
CompletableFuture
.supplyAsync(factorialTask, executor)
.thenApply(doubleTask)
.thenAccept(printTask);
System.out.println("Main thread works...");
System.out.println("Main thread finished...");
executor.close();
}
}Saída esperada:
Main thread started... Main thread works... Main thread finished... Final result: 240
A execução ocorre da seguinte forma:
supplyAsync()calcula o fatorial.thenApply()dobra o resultado.thenAccept()imprime o valor final.
Essa estrutura cria uma pipeline assíncrona, na qual cada etapa é executada após a conclusão da anterior, sem bloqueios na thread principal.
Resumo
CompletableFutureé uma extensão deFuturecom suporte a callbacks e composição de resultados.- Permite reagir automaticamente quando uma tarefa assíncrona termina.
- Pode usar o pool padrão (
ForkJoinPool) ou um executor personalizado. - Callbacks como
thenAccept(),thenApply()ethenRun()processam resultados sem bloquear. - Cadeias de callbacks permitem transformar e combinar valores de forma assíncrona.
- Evita o bloqueio da thread principal e facilita a programação reativa em Java.