CodeGym /Cursos /JAVA 25 SELF /thenCompose + Executor personalizado + timeouts

thenCompose + Executor personalizado + timeouts

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

1. thenCompose vs. thenApply: diferença e quando usar cada um

Na programação assíncrona em Java (com CompletableFuture), muitas vezes é necessário executar cadeias de ações. Para isso, há dois métodos parecidos: thenApply e thenCompose. Mas eles funcionam de maneiras diferentes!

thenApply

O método thenApply é usado quando o próximo passo é uma transformação simples do valor, sem iniciar novas operações assíncronas. Ele recebe o resultado da etapa anterior, o processa e retorna um novo valor (não um CompletableFuture).

Se você conhece a Stream API, então thenApply se comporta aproximadamente como map: pega o resultado, aplica uma função e retorna a versão transformada.

Exemplo:

CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> "42");
CompletableFuture<Integer> lengthFuture = cf.thenApply(s -> s.length());
// lengthFuture contém 2 (tamanho da string "42")

Em outras palavras, thenApply é uma forma de dizer: “Quando o resultado estiver pronto, faça isto com ele”.

thenCompose

  • Usado quando o próximo passo é outra operação assíncrona (retorna CompletableFuture).
  • Permite “desembrulhar” CompletableFuture aninhado (análogo a flatMap).
  • Se você usar thenApply com uma função assíncrona, obterá CompletableFuture<CompletableFuture<T>> — nada prático!

Exemplo:

CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> "user42");

// Suponha que precisemos obter os pedidos pelo nome de usuário (assíncrono)
CompletableFuture<List<Order>> ordersFuture = cf.thenCompose(username -> fetchOrdersAsync(username));
// fetchOrdersAsync retorna CompletableFuture<List<Order>>

Visualmente:

  • thenApply: CF<String>thenApply(s -> s.length())CF<Integer>
  • thenCompose: CF<User>thenCompose(u -> fetchOrdersAsync(u.id))CF<List<Order>>

Quando usar o quê?

  • A função retorna um valor comum — use thenApply.
  • A função retorna um CompletableFuture — use thenCompose.

Exemplo de erro:

cf.thenApply(username -> fetchOrdersAsync(username)); // Você obterá CF<CF<List<Order>>>
cf.thenCompose(username -> fetchOrdersAsync(username)); // Você obterá CF<List<Order>>

2. Gerenciamento de pool de threads (Executor): por que e como usar seu próprio Executor

Padrão: ForkJoinPool.commonPool()

Quando você escreve CompletableFuture.supplyAsync(...) ou thenApplyAsync(...) sem informar um Executor, o Java usa o pool de threads compartilhado — ForkJoinPool.commonPool(). Isso é conveniente, mas nem sempre adequado:

  • Se você tiver muitas operações longas ou bloqueantes (requisições de rede, trabalho com arquivos), o pool compartilhado pode ficar saturado e todas as tarefas vão esperar.
  • Às vezes, é necessário isolar tarefas com prioridades diferentes ou limitar a quantidade de threads executando simultaneamente.

Quando é necessário um Executor próprio?

  • Operações longas e bloqueantes (por exemplo, consultas ao banco, requisições HTTP, leitura de arquivos).
  • Isolamento de tarefas: para que tarefas do usuário não atrapalhem tarefas do sistema.
  • Limitação de recursos: por exemplo, não iniciar mais que 10 downloads simultâneos.

Como criar seu próprio Executor

Geralmente usa-se ThreadPoolExecutor ou fábricas de Executors:

ExecutorService myExecutor = Executors.newFixedThreadPool(10);

Como usar seu Executor com CompletableFuture

  • Nos métodos supplyAsync, runAsync, thenApplyAsync, thenComposeAsync e outros, é possível passar um segundo argumento — o seu Executor.

Exemplos:

CompletableFuture<String> cf = CompletableFuture.supplyAsync(
    () -> loadDataFromNetwork(), myExecutor
);

cf.thenApplyAsync(data -> processData(data), myExecutor)
  .thenAcceptAsync(result -> System.out.println(result), myExecutor);

Importante: se você não informar um Executor, será usado o ForkJoinPool.commonPool().

Quando o Executor padrão é suficiente?

  • Para tarefas curtas, CPU-bound (cálculos simples).
  • Quando não importa em qual thread a tarefa será executada.

3. Tratamento de timeouts: orTimeout e completeOnTimeout

Operações assíncronas podem travar ou demorar demais (por exemplo, se o servidor não responder). Para não esperar para sempre, no CompletableFuture existem métodos para trabalhar com timeouts.

orTimeout

  • Finaliza o CompletableFuture com a exceção TimeoutException se a operação não terminar no tempo especificado.
  • Não cancela a tarefa que está em execução, mas a cadeia subsequente receberá o erro.

Sintaxe:

cf.orTimeout(3, TimeUnit.SECONDS)
  .exceptionally(ex -> {
      System.out.println("Tempo limite: " + ex);
      return null;
  });

Exemplo:

CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
    Thread.sleep(5000); // simulamos uma operação demorada
    return "OK";
});

cf.orTimeout(2, TimeUnit.SECONDS)
  .exceptionally(ex -> {
      System.out.println("Erro: " + ex);
      return "TIMEOUT";
  });

Resultado:

Após 2 segundos será lançada TimeoutException, e exceptionally tratará o erro.

completeOnTimeout

  • Finaliza o CompletableFuture com o valor especificado, se a operação não terminar dentro do tempo limite.
  • Não lança exceção; retorna um valor “de reserva”.

Sintaxe:

cf.completeOnTimeout("DEFAULT", 2, TimeUnit.SECONDS);

Exemplo:

CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
    Thread.sleep(5000);
    return "OK";
});

cf.completeOnTimeout("TIMEOUT", 2, TimeUnit.SECONDS)
  .thenAccept(System.out::println); // Em 2 segundos imprimirá "TIMEOUT"

Comparação entre orTimeout e completeOnTimeout

Método O que faz no timeout? Como é tratado depois?
orTimeout
Finaliza com TimeoutException Pode ser tratado via exceptionally/handle
completeOnTimeout
Finaliza com o valor especificado thenAccept/thenApply receberá esse valor

4. Prática: exemplo com thenCompose, Executor personalizado e timeout

Tarefa:

  • Obter o usuário por id (assíncrono, com atraso).
  • Em seguida, obter assíncronamente a lista de pedidos do usuário (também com atraso).
  • Usar um Executor personalizado.
  • Adicionar timeout na obtenção dos pedidos.
import java.util.concurrent.*;
import java.util.*;

public class AsyncDemo {
    static ExecutorService ioExecutor = Executors.newFixedThreadPool(4);

    // Simulação de obtenção assíncrona do usuário
    static CompletableFuture<String> fetchUserAsync(int userId) {
        return CompletableFuture.supplyAsync(() -> {
            sleep(1000);
            return "user" + userId;
        }, ioExecutor);
    }

    // Simulação de obtenção assíncrona dos pedidos do usuário
    static CompletableFuture<List<String>> fetchOrdersAsync(String username) {
        return CompletableFuture.supplyAsync(() -> {
            sleep(3000); // Operação demorada!
            return List.of("order1", "order2");
        }, ioExecutor);
    }

    static void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException ignored) {}
    }

    public static void main(String[] args) {
        fetchUserAsync(42)
            .thenCompose(username ->
                fetchOrdersAsync(username)
                    .orTimeout(2, TimeUnit.SECONDS) // Timeout ao obter os pedidos
                    .exceptionally(ex -> {
                        System.out.println("Não foi possível obter os pedidos: " + ex);
                        return List.of();
                    })
            )
            .thenAccept(orders -> System.out.println("Pedidos: " + orders))
            .join(); // Aguardamos a conclusão de toda a cadeia

        ioExecutor.shutdown();
    }
}

O que acontece:

  • Obtemos o usuário (1 segundo).
  • Obtemos os pedidos (3 segundos, mas o timeout é de 2 segundos).
  • Se não der tempo — capturamos TimeoutException e retornamos uma lista vazia.
  • Tudo roda em um Executor personalizado.

Resultado:

Não foi possível obter os pedidos: java.util.concurrent.TimeoutException
Pedidos: []

Se você reduzir o atraso em fetchOrdersAsync para 1_000 ms — verá os pedidos reais.

5. Erros comuns e nuances

Erro nº 1: usar thenApply em vez de thenCompose para operações assíncronas.
Se a função retorna um CompletableFuture, mas você aplicou thenApply, terá o tipo aninhado CompletableFuture<CompletableFuture<T>>. Isso complica a cadeia e leva a wrappers desnecessários. Solução: use thenCompose para “achatar” o resultado em CompletableFuture<T>.

Erro nº 2: executar tarefas longas ou de E/S sem um Executor próprio.
Por padrão, as tarefas são executadas em ForkJoinPool.commonPool(). Se ele ficar sobrecarregado, as latências começam a crescer e outras tarefas do aplicativo podem desacelerar. Solução: crie seu próprio ExecutorService e passe-o para supplyAsync/thenApplyAsync.

Erro nº 3: esperar que orTimeout cancele a execução da tarefa.
orTimeout apenas finaliza o CompletableFuture com exceção por timeout, mas a tarefa continua em segundo plano. Solução: se precisar interromper a execução, use cancel(true) ou mecanismos próprios de interrupção.

Erro nº 4: entender incorretamente o escopo do timeout.
orTimeout e completeOnTimeout funcionam apenas para uma etapa específica da cadeia, não para a cadeia toda. Solução: se precisar de um timeout geral para toda a cadeia, envolva-a em um CompletableFuture separado e aplique o timeout a ele.

Erro nº 5: não fechar o ExecutorService.
Se após executar as tarefas você não chamar shutdown()/shutdownNow() no ExecutorService, as threads continuarão ativas e o programa pode ficar bloqueado. Solução: sempre feche o ExecutorService em finally ou use try-with-resources no Java 21+.

1
Pesquisa/teste
Programação assíncrona, nível 55, lição 4
Indisponível
Programação assíncrona
Programação assíncrona
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION