CodeGym /Cursos /JAVA 25 SELF /Tarefas assíncronas: thenApply, thenAccept, thenRun

Tarefas assíncronas: thenApply, thenAccept, thenRun

JAVA 25 SELF
Nível 55 , Lição 1
Disponível

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
thenApply
Sim Sim Transformação do resultado
thenAccept
Sim Não Efeitos colaterais (saída, log)
thenRun
Não Não Apenas uma ação após a conclusão da tarefa
thenApplyAsync
Sim Sim O mesmo, mas em outra thread
thenAcceptAsync
Sim Não O mesmo, mas em outra thread
thenRunAsync
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.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION