1. Inicializando uma tarefa assíncrona: supplyAsync e runAsync
A maneira mais comum de iniciar uma tarefa assíncrona — é usar CompletableFuture.supplyAsync. Esse método recebe uma lambda ou um método que retorna um resultado. Por exemplo, queremos simular o carregamento de dados do servidor:
import java.util.concurrent.CompletableFuture;
public class Main {
public static void main(String[] args) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// Simulação de uma operação demorada (por exemplo, download de arquivo)
sleep(1000);
return "Dados do servidor";
});
System.out.println("Tarefa iniciada!");
// ... aqui você pode fazer outra coisa enquanto a tarefa é executada
}
private static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException ignored) {}
}
}
runAsync: quando o resultado não é necessário
Se sua tarefa não retorna nada (por exemplo, apenas escreve no log, envia uma notificação), use runAsync:
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
sleep(500);
System.out.println("Operação concluída!");
});
runAsync sempre retorna CompletableFuture<Void>, porque não há resultado esperado.
2. thenApply, thenAccept, thenRun: qual é a diferença?
Quando a tarefa assíncrona termina, geralmente queremos fazer algo com o resultado. Para isso existem os métodos “manipuladores”:
- thenApply — transforma o resultado e retorna um novo resultado.
- thenAccept — consome o resultado, não retorna nada (usado para efeitos colaterais).
- thenRun — não recebe o resultado e não retorna nada (apenas executa uma ação após a conclusão da tarefa).
thenApply: processamento e transformação do resultado
Se você precisa transformar o resultado da tarefa anterior, use thenApply. Por exemplo, carregamos uma string e agora queremos saber seu tamanho:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Java");
CompletableFuture<Integer> lengthFuture = future.thenApply(s -> {
System.out.println("Calculando o tamanho da string...");
return s.length();
});
// lengthFuture agora contém Integer — o tamanho da string "Java"
lengthFuture.thenAccept(len -> System.out.println("Tamanho: " + len));
O que acontece:
- future contém a string "Java".
- thenApply transforma a string no seu tamanho (int).
- thenAccept imprime o resultado.
thenAccept: ação com o resultado (não retorna nada)
Se você só precisa fazer algo com o resultado (por exemplo, exibi-lo), e não precisa retornar nada — use thenAccept:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Olá, mundo!");
future.thenAccept(result -> {
System.out.println("Resultado: " + result);
});
thenAccept é como um “consumidor”: ele consome o resultado e faz algo útil com ele.
thenRun: ação sem resultado
Se você quer apenas executar uma ação após a conclusão da tarefa, mas não precisa do resultado, use thenRun:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Pronto!");
future.thenRun(() -> {
System.out.println("Carregamento concluído!");
});
Observe: dentro de thenRun você não tem acesso ao resultado da tarefa anterior — ele simplesmente é ignorado.
3. Cadeias de chamadas: construindo um pipeline de tarefas
A maior força de CompletableFuture — é a capacidade de construir cadeias de computações. Cada método (thenApply, thenAccept, thenRun) retorna um novo CompletableFuture, ao qual você pode adicionar outro handler.
Exemplo: processamento em múltiplas etapas
Vamos melhorar nosso aplicativo: carregar dados, transformá-los, exibir o resultado e escrever no log que tudo foi concluído.
CompletableFuture.supplyAsync(() -> {
System.out.println("Etapa 1: Carregando dados...");
sleep(500);
return "Java";
})
.thenApply(data -> {
System.out.println("Etapa 2: Transformando os dados...");
return data.toUpperCase();
})
.thenAccept(result -> {
System.out.println("Etapa 3: Exibindo o resultado: " + result);
})
.thenRun(() -> {
System.out.println("Etapa 4: Tudo concluído!");
});
Saída no console:
Etapa 1: Carregando dados...
Etapa 2: Transformando os dados...
Etapa 3: Exibindo o resultado: JAVA
Etapa 4: Tudo concluído!
Observe:
Cada etapa seguinte começa apenas após a conclusão da anterior. Isso permite construir verdadeiros “pipelines” de processamento de dados.
4. Versões assíncronas: thenApplyAsync, thenAcceptAsync, thenRunAsync
Por padrão, os handlers (thenApply, thenAccept, thenRun) são executados na mesma thread em que a tarefa anterior foi concluída. Às vezes isso não é muito conveniente — se o processamento for pesado, é melhor movê-lo para outra thread.
Para isso existem as versões assíncronas:
- thenApplyAsync
- thenAcceptAsync
- thenRunAsync
Qual é a diferença?
- Sem Async: o handler pode ser executado na mesma thread da tarefa anterior (por exemplo, se a tarefa foi concluída no ForkJoinPool, o handler roda lá também).
- Com Async: o handler será executado, com garantia, em outra thread do ForkJoinPool (ou do seu Executor).
Exemplo: comparar handler normal e assíncrono
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
System.out.println("Carregando... [" + Thread.currentThread().getName() + "]");
return "Hello";
});
future.thenApply(result -> {
System.out.println("thenApply: [" + Thread.currentThread().getName() + "]");
return result + " World";
});
future.thenApplyAsync(result -> {
System.out.println("thenApplyAsync: [" + Thread.currentThread().getName() + "]");
return result + " Async World";
});
Saída típica:
Carregando... [ForkJoinPool.commonPool-worker-1]
thenApply: [ForkJoinPool.commonPool-worker-1]
thenApplyAsync: [ForkJoinPool.commonPool-worker-2]
Conclusão:
O handler assíncrono é executado em outra thread.
Quando usar os métodos Async?
- Se o processamento for pesado (por exemplo, computações complexas, trabalho de rede).
- Se você não quiser bloquear a thread na qual a tarefa anterior foi concluída.
- Se quiser gerenciar explicitamente as threads (por exemplo, passando seu próprio Executor como segundo argumento).
5. Dicas úteis
Tabela: comparação dos métodos thenApply, thenAccept, thenRun
| Método | Usa o resultado? | Retorna valor? | Para que usar |
|---|---|---|---|
|
Sim | Sim | Transformação do resultado |
|
Sim | Não | Efeitos colaterais (saída, log) |
|
Não | Não | Apenas uma ação após a conclusão da tarefa |
|
Sim | Sim | O mesmo, mas em outra thread |
|
Sim | Não | O mesmo, mas em outra thread |
|
Não | Não | O mesmo, mas em outra thread |
Pergunta: como construir cadeias longas?
É possível chamar os métodos um após o outro, como blocos de LEGO:
CompletableFuture.supplyAsync(() -> "42")
.thenApply(Integer::parseInt)
.thenApply(x -> x * 2)
.thenAccept(x -> System.out.println("Resultado: " + x));
Saída:
Resultado: 84
Cada etapa seguinte recebe o resultado da anterior, podendo modificá-lo ou apenas utilizá-lo.
6. Erros comuns ao trabalhar com thenApply, thenAccept, thenRun
Erro nº 1: Confusão nos tipos de retorno.
thenApply deve retornar um valor que seguirá pela cadeia. Se você usar thenApply mas não retornar um resultado, a próxima operação receberá null (ou nem vai compilar). Para efeitos colaterais, use thenAccept ou thenRun.
Erro nº 2: Tentar usar o resultado em thenRun.
Dentro de thenRun não há acesso ao resultado da tarefa anterior. Se você precisa usar o resultado, escolha thenApply ou thenAccept.
Erro nº 3: Bloquear a thread principal.
Se você chama get() ou join() na thread principal, perde todas as vantagens da assincronicidade: a thread ficará esperando a conclusão da tarefa, como no bom e velho código síncrono. É melhor usar cadeias não bloqueantes e callbacks.
Erro nº 4: Falta de tratamento de erros.
Se ocorrer uma exceção na cadeia e você não adicionar um handler (exceptionally, handle, whenComplete), ela “se perde”, e a tarefa pode terminar com erro que você não verá. Sempre trate erros nas cadeias.
Erro nº 5: Execução inesperada em outra thread.
Os métodos assíncronos (thenApplyAsync e outros) podem ser executados em outra thread. Se você acessar variáveis sem proteção para acesso concorrente, podem ocorrer condições de corrida.
GO TO FULL VERSION