Como usar o CompletableFuture no Java: Guia Prático para Programação Assíncrona

O uso do CompletableFuture em Java é uma maneira eficaz de trabalhar com operações assíncronas, permitindo que os desenvolvedores escrevam código mais limpo e legível. A principal vantagem do CompletableFuture é a sua capacidade de facilitar o processamento paralelo e lidar com múltiplas operações assíncronas de forma simples. Com seu uso, é possível executar tarefas em background sem bloquear a thread principal, melhorando a eficiência das aplicações.

Além disso, o CompletableFuture oferece uma API rica que permite combinar várias operações assíncronas e manipular resultados de forma intuitiva. Essa funcionalidade torna-o um recurso poderoso para desenvolvedores que buscam otimizar o desempenho de suas aplicações Java. O artigo a seguir fornecerá uma visão geral detalhada e exemplos práticos sobre como utilizar o CompletableFuture no dia a dia da programação.

Fundamentos do CompletableFuture

O CompletableFuture é uma ferramenta poderosa em Java para programação assíncrona. Este componente permite que tarefas sejam executadas de forma não bloqueante, facilitando a criação de aplicações mais responsivas.

O que é CompletableFuture

O CompletableFuture é uma implementação da interface Future, que permite que os desenvolvedores executem operações de forma assíncrona. Ele facilita a escrita de código assíncrono em Java, permitindo encadear ações que devem ser realizadas após a conclusão da tarefa inicial. Por exemplo, uma operação de rede pode ser iniciada, e enquanto aguarda a resposta, outras tarefas podem ser processadas.

Além disso, ele oferece métodos como supplyAsync, thenApply, e exceptionally, que tornam a manipulação de resultados e exceções mais simples. O uso deste framework melhora a legibilidade e a manutenção do código.

Vantagens do uso de CompletableFuture

As vantagens do CompletableFuture incluem maior facilidade no tratamento de múltiplas tarefas simultaneamente. Ele permite que várias operações sejam realizadas em paralelo, aumentando a eficiência do programa. Com o uso do método allOf, o desenvolvedor pode esperar por várias CompletableFutures concluírem antes de prosseguir.

Outro benefício é a facilidade de composição. Tarefas podem ser encadeadas usando métodos como thenCompose, proporcionando uma estrutura clara e lógica para o fluxo de execução. Isso minimiza o uso de callbacks aninhados, um problema comum em outras abordagens.

Diferenças entre CompletableFuture e Future

Enquanto o Future proporciona um modo básico de trabalhar com tarefas assíncronas, ele carece de métodos para compor e lidar com a execução de forma mais eficiente. O CompletableFuture vai além, permitindo não apenas esperar pelo resultado, mas também especificar ações a serem realizadas após a conclusão da tarefa.

Além disso, um Future é limitado a operações que bloqueiam a thread até que a tarefa seja completada. Por outro lado, o CompletableFuture é projetado para ser não bloqueante. Isso resulta em uma melhor performance em aplicações que precisam de alta responsividade e gestão eficiente de recursos.

Trabalhando com CompletableFuture

Ao trabalhar com CompletableFuture, o programador pode realizar operações assíncronas de modo simples e funcional. A seguir, são abordadas maneiras de criar, completar e encadear CompletableFuture, além de como lidar com exceções.

Criando CompletableFuture

Existem várias formas de criar um CompletableFuture, pode-se usar o método estático CompletableFuture.supplyAsync(), que executa uma tarefa assíncrona. Este método aceita um Supplier, que representa uma função que pode ser executada de forma assíncrona.

Por exemplo:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    // tarefa longa
    return "Resultado";
});
Java

Ou criar da forma mais básica, que seria criando uma instancia de forma manual.

CompletableFuture<String> future = new CompletableFuture<>();
Java

Além disso, CompletableFuture.runAsync() é útil quando não se espera um resultado, mas sim apenas a execução de uma tarefa:

CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    // executar ação
});
Java

Completando um CompletableFuture

Um CompletableFuture pode ser completado manualmente usando o método complete(). Isso é útil em situações onde se deseja forçar a conclusão de uma tarefa:

CompletableFuture<String> future = new CompletableFuture<>();
future.complete("resultado pronto");
Java

O estado do CompletableFuture muda para completo, permitindo que o resultado seja acessado. Se a execução assíncrona foi concluída, isso não afeta o resultado, que pode ser obtido com get().

Encadeando CompletableFuture com thenApply, thenAccept, e thenRun

Encadear operações é uma das principais funcionalidades do CompletableFuture. O método thenApply() transforma o resultado de uma operação anterior:

future.thenApply(result -> {
    return result.toUpperCase();
});
Java

thenAccept() é semelhante, mas não retorna um valor; ele apenas consome o resultado:

future.thenAccept(result -> {
    System.out.println("Resultado: " + result);
});
Java

Por outro lado, thenRun() não recebe um resultado, executando uma ação após a conclusão da tarefa original:

future.thenRun(() -> {
    System.out.println("Tarefa concluída!");
});
Java

Exemplo de uso:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Primeiro resultado")
    .thenApply(resultado -> resultado + " e mais um passo.")
    .thenApply(resultado -> resultado + " Finalizado.");

future.thenAccept(System.out::println); // Imprime o resultado final
Java

Combinando CompletableFuture com thenCombine e thenCompose

Para combinar dois CompletableFuture, utiliza-se thenCombine(), que permite operar sobre dois resultados:

CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> 2);
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> 3);

future1.thenCombine(future2, (result1, result2) -> result1 + result2)
       .thenAccept(System.out::println);
Java

thenCompose() é útil para encadear tarefas que retornam CompletableFuture, permitindo uma forma mais fluida de criar chamadas em sequência:

future1.thenCompose(result -> CompletableFuture.supplyAsync(() -> result * 2));
Java

Mais exemplos:

// Combinando dois futuros com thenCombine
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Resultado 1");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "Resultado 2");

CompletableFuture<String> combinedFuture = future1.thenCombine(future2, (res1, res2) -> res1 + " e " + res2);
combinedFuture.thenAccept(System.out::println);  // Imprime: Resultado 1 e Resultado 2

// Usando allOf para esperar por múltiplos futuros
CompletableFuture<Void> allFutures = CompletableFuture.allOf(future1, future2);
allFutures.thenRun(() -> System.out.println("Todos os futuros foram completados."));
Java

Tratamento de exceções com exceptionally e handle

Para gerenciar exceções em operações assíncronas, exceptionally() fornece uma maneira de lidar com falhas:

future.exceptionally(ex -> {
    System.err.println("Erro: " + ex.getMessage());
    return "Valor padrão";
});
Java

O método handle() permite lidar com resultados e exceções, fornecendo um tratamento mais flexível:

future.handle((result, ex) -> {
    if (ex != null) {
        return "Erro tratado";
    }
    return result;
});
Java

Essas abordagens asseguram que o fluxo de execução permaneça claro e controlado, mesmo em caso de falhas.

Bloqueando a execução até a conclusão do futuro

Você pode bloquear a thread principal até que o futuro seja concluído usando os métodos get ou join

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Resultado assíncrono");
try {
    String resultado = future.get(); // Bloqueia até que o resultado esteja disponível
    System.out.println(resultado);
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}
Java

Conclusão

O CompletableFuture é uma ferramenta poderosa para lidar com operações assíncronas em Java. Ele suporta diversos padrões de programação assíncrona, como encadeamento, tratamento de exceções e combinações de múltiplos futuros, tornando o código mais eficiente e legível.

Links úteis

Artefato X
Artefato X
Artigos: 12