Guia para CompletableFuture

Guia para CompletableFuture

1. Introdução

Este artigo é um guia para a funcionalidade e casos de uso da classeCompletableFuture - introduzida como uma melhoria da API de simultaneidade do Java 8.

Leitura adicional:

Executável vs. Chamadas em Java

Aprenda a diferença entre as interfaces Runnable e Callable em Java.

Read more

Guia para java.util.concurrent.Future

Um guia para java.util.concurrent.Future com uma visão geral de suas diversas implementações

Read more

2. Computação Assíncrona em Java

É difícil pensar em computação assíncrona. Normalmente, queremos pensar em qualquer cálculo como uma série de etapas. Mas no caso de computação assíncrona,actions represented as callbacks tend to be either scattered across the code or deeply nested inside each other. As coisas pioram ainda mais quando precisamos lidar com erros que podem ocorrer durante uma das etapas.

A interfaceFuture foi adicionada em Java 5 para servir como resultado de um cálculo assíncrono, mas não tinha nenhum método para combinar esses cálculos ou tratar possíveis erros.

In Java 8, the CompletableFuture class was introduced. Junto com a interfaceFuture, ele também implementou a interfaceCompletionStage. Essa interface define o contrato para uma etapa de computação assíncrona que pode ser combinada com outras etapas.

CompletableFuture é ao mesmo tempo um bloco de construção e uma estrutura comabout 50 different methods for composing, combining, executing asynchronous computation steps and handling errors.

Uma API tão grande pode ser esmagadora, mas ocorre principalmente em vários casos de uso claros e distintos.

3. UsandoCompletableFuture como umFuture Simples

Primeiro de tudo, a classeCompletableFuture implementa a interfaceFuture, então você podeuse it as a Future implementation, but with additional completion logic.

Por exemplo, você pode criar uma instância desta classe com um construtor sem argumentos para representar algum resultado futuro, distribuí-lo aos consumidores e completá-lo em algum momento no futuro usando o métodocomplete. Os consumidores podem usar o métodoget para bloquear o encadeamento atual até que esse resultado seja fornecido.

No exemplo a seguir, temos um método que cria uma instânciaCompletableFuture, depois executa alguns cálculos em outra thread e retornaFuture imediatamente.

Quando o cálculo é feito, o método completaFuture fornecendo o resultado ao métodocomplete:

public Future calculateAsync() throws InterruptedException {
    CompletableFuture completableFuture
      = new CompletableFuture<>();

    Executors.newCachedThreadPool().submit(() -> {
        Thread.sleep(500);
        completableFuture.complete("Hello");
        return null;
    });

    return completableFuture;
}

Para girar a computação, usamos a APIExecutor que é descrita no artigo“Introduction to Thread Pools in Java”, mas este método de criar e completar umCompletableFuture pode ser usado junto com qualquer mecanismo de simultaneidade ou API incluindo tópicos brutos.

Observe quethe calculateAsync method returns a Future instance.

Simplesmente chamamos o método, recebemos a instânciaFuture e chamamos o métodoget quando estivermos prontos para bloquear o resultado.

Observe também que o métodoget lança algumas exceções verificadas, a saberExecutionException (encapsulando uma exceção que ocorreu durante um cálculo) eInterruptedException (uma exceção significando que uma thread executando um método foi interrompida) :

Future completableFuture = calculateAsync();

// ...

String result = completableFuture.get();
assertEquals("Hello", result);

If you already know the result of a computation, você pode usar o métodocompletedFuture estático com um argumento que representa um resultado desse cálculo. Então, o métodoget deFuture nunca bloqueará, retornando imediatamente esse resultado.

Future completableFuture =
  CompletableFuture.completedFuture("Hello");

// ...

String result = completableFuture.get();
assertEquals("Hello", result);

Como um cenário alternativo, você pode querercancel the execution of a Future.

Suponha que não conseguimos encontrar um resultado e decidimos cancelar uma execução assíncrona completamente. Isso pode ser feito com o métodoFuture'scancel. Este método recebe um argumentobooleanmayInterruptIfRunning, mas no caso deCompletableFuture não tem efeito, pois as interrupções não são usadas para controlar o processamento deCompletableFuture.

Esta é uma versão modificada do método assíncrono:

public Future calculateAsyncWithCancellation() throws InterruptedException {
    CompletableFuture completableFuture = new CompletableFuture<>();

    Executors.newCachedThreadPool().submit(() -> {
        Thread.sleep(500);
        completableFuture.cancel(false);
        return null;
    });

    return completableFuture;
}

Quando bloqueamos o resultado usando o métodoFuture.get(), ele lançaCancellationException se o futuro for cancelado:

Future future = calculateAsyncWithCancellation();
future.get(); // CancellationException

4. CompletableFuture com lógica de computação encapsulada

O código acima nos permite escolher qualquer mecanismo de execução simultânea, mas e se quisermos pular esse clichê e simplesmente executar algum código de forma assíncrona?

Os métodos estáticosrunAsyncesupplyAsync nos permitem criar uma instânciaCompletableFuture dos tipos funcionaisRunnableeSupplier correspondentemente.

TantoRunnableeSupplier são interfaces funcionais que permitem passar suas instâncias como expressões lambda graças ao novo recurso Java 8.

A interfaceRunnable é a mesma interface antiga usada em threads e não permite retornar um valor.

A interfaceSupplier é uma interface funcional genérica com um único método que não possui argumentos e retorna um valor de um tipo parametrizado.

Isso permiteprovide an instance of the Supplier as a lambda expression that does the calculation and returns the result. Isto é tão simples quanto:

CompletableFuture future
  = CompletableFuture.supplyAsync(() -> "Hello");

// ...

assertEquals("Hello", future.get());

5. Processando resultados de cálculos assíncronos

A maneira mais genérica de processar o resultado de uma computação é alimentá-lo com uma função. O métodothenApply faz exatamente isso: aceita uma instânciaFunction, usa-a para processar o resultado e retorna umFuture que contém um valor retornado por uma função:

CompletableFuture completableFuture
  = CompletableFuture.supplyAsync(() -> "Hello");

CompletableFuture future = completableFuture
  .thenApply(s -> s + " World");

assertEquals("Hello World", future.get());

Se você não precisa retornar um valor na cadeiaFuture, pode usar uma instância da interface funcionalConsumer. Seu único método recebe um parâmetro e retornavoid.

Há um método para este caso de uso emCompletableFuture - o métodothenAccept recebe umConsumere passa o resultado do cálculo. A chamada finalfuture.get() retorna uma instância do tipoVoid.

CompletableFuture completableFuture
  = CompletableFuture.supplyAsync(() -> "Hello");

CompletableFuture future = completableFuture
  .thenAccept(s -> System.out.println("Computation returned: " + s));

future.get();

Por fim, se você não precisa do valor do cálculo nem deseja retornar algum valor no final da cadeia, pode passar um lambdaRunnable para o métodothenRun. No exemplo a seguir, depois que o métodofuture.get() é chamado, simplesmente imprimimos uma linha no console:

CompletableFuture completableFuture
  = CompletableFuture.supplyAsync(() -> "Hello");

CompletableFuture future = completableFuture
  .thenRun(() -> System.out.println("Computation finished."));

future.get();

6. Combinando Futuros

A melhor parte da APICompletableFuture éability to combine CompletableFuture instances in a chain of computation steps.

O resultado desse encadeamento é ele próprio umCompletableFuture que permite mais encadeamento e combinação. Essa abordagem é onipresente em linguagens funcionais e geralmente é chamada de padrão de projeto monádico.

No exemplo a seguir, usamos o métodothenCompose para encadear doisFutures sequencialmente.

Observe que este método recebe uma função que retorna uma instânciaCompletableFuture. O argumento desta função é o resultado da etapa de cálculo anterior. Isso nos permite usar esse valor dentro do lambda do próximoCompletableFuture:

CompletableFuture completableFuture
  = CompletableFuture.supplyAsync(() -> "Hello")
    .thenCompose(s -> CompletableFuture.supplyAsync(() -> s + " World"));

assertEquals("Hello World", completableFuture.get());

O métodothenCompose junto comthenApply implementam blocos de construção básicos do padrão monádico. Eles estão intimamente relacionados aos métodosmapeflatMap das classesStreameOptional também disponíveis em Java 8.

Ambos os métodos recebem uma função e a aplicam ao resultado do cálculo, mas o métodothenCompose (flatMap)receives a function that returns another object of the same type. Essa estrutura funcional permite compor as instâncias dessas classes como blocos de construção.

Se você quiser executar doisFutures independentes e fazer algo com seus resultados, use o métodothenCombine que aceitaFutureeFunction com dois argumentos para processar ambos os resultados:

CompletableFuture completableFuture
  = CompletableFuture.supplyAsync(() -> "Hello")
    .thenCombine(CompletableFuture.supplyAsync(
      () -> " World"), (s1, s2) -> s1 + s2));

assertEquals("Hello World", completableFuture.get());

Um caso mais simples é quando você quer fazer algo com dois resultadosFutures, mas não precisa passar nenhum valor resultante por uma cadeiaFuture. O métodothenAcceptBoth está aí para ajudar:

CompletableFuture future = CompletableFuture.supplyAsync(() -> "Hello")
  .thenAcceptBoth(CompletableFuture.supplyAsync(() -> " World"),
    (s1, s2) -> System.out.println(s1 + s2));

7. Diferença entrethenApply() ethenCompose()

Em nossas seções anteriores, mostramos exemplos sobrethenApply() ethenCompose(). Ambas as APIs ajudam a encadear diferentes chamadasCompletableFuture, mas o uso dessas 2 funções é diferente.

7.1. thenApply()

This method is used for working with a result of the previous call. No entanto, um ponto importante a lembrar é que o tipo de retorno será combinado com todas as chamadas.

Portanto, este método é útil quando queremos transformar o resultado de uma escaladaCompletableFuture :

CompletableFuture finalResult = compute().thenApply(s-> s + 1);

7.2. thenCompose()

O métodothenCompose() é semelhante athenApply() em que ambos retornam um novo Estágio de Conclusão. No entanto,thenCompose() uses the previous stage as the argument. Ele vai nivelar e retornar umFuture com o resultado diretamente, ao invés de um futuro aninhado como observamos emthenApply():

CompletableFuture computeAnother(Integer i){
    return CompletableFuture.supplyAsync(() -> 10 + i);
}
CompletableFuture finalResult = compute().thenCompose(this::computeAnother);

Portanto, se a ideia é encadear os métodosCompletableFuture, é melhor usarthenCompose().

Além disso, observe que a diferença entre esses dois métodos é análoga ahttps://www.example.com/java-difference-map-and-flatmap.

8. Executando váriosFutures em paralelo

Quando precisamos executar váriosFutures em paralelo, geralmente queremos esperar que todos eles sejam executados e, em seguida, processar seus resultados combinados.

O método estáticoCompletableFuture.allOf permite aguardar a conclusão de todos osFutures fornecidos como um var-arg:

CompletableFuture future1
  = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture future2
  = CompletableFuture.supplyAsync(() -> "Beautiful");
CompletableFuture future3
  = CompletableFuture.supplyAsync(() -> "World");

CompletableFuture combinedFuture
  = CompletableFuture.allOf(future1, future2, future3);

// ...

combinedFuture.get();

assertTrue(future1.isDone());
assertTrue(future2.isDone());
assertTrue(future3.isDone());

Observe que o tipo de retorno deCompletableFuture.allOf() é aCompletableFuture<Void>. A limitação desse método é que ele não retorna os resultados combinados de todos osFutures. Em vez disso, você deve obter manualmente os resultados deFutures. Felizmente, o métodoCompletableFuture.join() e a API Java 8 Streams tornam isso simples:

String combined = Stream.of(future1, future2, future3)
  .map(CompletableFuture::join)
  .collect(Collectors.joining(" "));

assertEquals("Hello Beautiful World", combined);

O métodoCompletableFuture.join() é semelhante ao métodoget, mas lança uma exceção não verificada no caso deFuture não ser concluído normalmente. Isso torna possível usá-lo como uma referência de método no métodoStream.map().

9. Tratamento de erros

Para tratamento de erros em uma cadeia de etapas de computação assíncrona, o idiomathrow/catch teve que ser adaptado de maneira semelhante.

Em vez de capturar uma exceção em um bloco sintático, a classeCompletableFuture permite que você a trate em um métodohandle especial. Este método recebe dois parâmetros: resultado de uma computação (se concluída com êxito) e a exceção lançada (se alguma etapa da computação não tiver sido concluída normalmente).

No exemplo a seguir, usamos o métodohandle para fornecer um valor padrão quando o cálculo assíncrono de uma saudação foi concluído com um erro porque nenhum nome foi fornecido:

String name = null;

// ...

CompletableFuture completableFuture
  =  CompletableFuture.supplyAsync(() -> {
      if (name == null) {
          throw new RuntimeException("Computation error!");
      }
      return "Hello, " + name;
  })}).handle((s, t) -> s != null ? s : "Hello, Stranger!");

assertEquals("Hello, Stranger!", completableFuture.get());

Como um cenário alternativo, suponha que desejemos completar manualmenteFuture com um valor, como no primeiro exemplo, mas também ter a capacidade de completá-lo com uma exceção. O métodocompleteExceptionally se destina a isso. O métodocompletableFuture.get() no exemplo a seguir lança umExecutionException comRuntimeException como causa:

CompletableFuture completableFuture = new CompletableFuture<>();

// ...

completableFuture.completeExceptionally(
  new RuntimeException("Calculation failed!"));

// ...

completableFuture.get(); // ExecutionException

No exemplo acima, poderíamos ter tratado a exceção com o métodohandle assincronamente, mas com o métodoget podemos usar uma abordagem mais típica de processamento de exceção síncrona.

10. Métodos Assíncronos

A maioria dos métodos da API fluente na classeCompletableFuture tem duas variantes adicionais com o postfixAsync. Esses métodos geralmente são destinados arunning a corresponding step of execution in another thread.

Os métodos sem o postfixAsync executam o próximo estágio de execução usando um thread de chamada. O métodoAsync sem o argumentoExecutor executa uma etapa usando a implementação de poolfork/join comum deExecutor que é acessada com o métodoForkJoinPool.commonPool(). O métodoAsync com um argumentoExecutor executa uma etapa usando oExecutor passado.

Aqui está um exemplo modificado que processa o resultado de um cálculo com uma instânciaFunction. A única diferença visível é o métodothenApplyAsync. Mas, por baixo do capô, a aplicação de uma função está incluída em uma instânciaForkJoinTask (para obter mais informações sobre a estruturafork/join, consulte o artigo“Guide to the Fork/Join Framework in Java”). Isso permite paralelizar ainda mais sua computação e usar os recursos do sistema com mais eficiência.

CompletableFuture completableFuture
  = CompletableFuture.supplyAsync(() -> "Hello");

CompletableFuture future = completableFuture
  .thenApplyAsync(s -> s + " World");

assertEquals("Hello World", future.get());

11. API JDK 9CompletableFuture

No Java 9, a APICompletableFuture foi aprimorada ainda mais com as seguintes alterações:

  • Novos métodos de fábrica adicionados

  • Suporte para atrasos e tempos limite

  • Suporte aprimorado para subclassificação.

Novas APIs de instância foram introduzidas:

  • Executor defaultExecutor ()

  • CompletableFuture newIncompleteFuture ()

  • CompletableFuture copy ()

  • CompletionStage minimalCompletionStage ()

  • CompletableFuture completeAsync (Fornecedor fornecedor, executor executor)

  • CompletableFuture completeAsync (Fornecedor fornecedor)

  • CompletableFuture ouTimeout (tempo limite longo, unidade TimeUnit)

  • CompletableFuture completeOnTimeout (valor T, tempo limite longo, unidade TimeUnit)

Agora também temos alguns métodos de utilidade estática:

  • Executor atrasadoExecutor (atraso longo, unidade TimeUnit, executor Executor)

  • Executor atrasadoExecutor (atraso longo, unidade TimeUnit)

  • CompletionStage completedStage (valor U)

  • CompletionStage failedStage (ex lançável)

  • CompletableFuture failedFuture (ex lançável)

Por fim, para resolver o tempo limite, o Java 9 introduziu mais duas novas funções:

  • orTimeout ()

  • completeOnTimeout ()

Aqui está o artigo detalhado para leitura adicional:Java 9 CompletableFuture API Improvements.

12. Conclusão

Neste artigo, descrevemos os métodos e casos de uso típicos da classeCompletableFuture.

O código-fonte do artigo está disponívelover on GitHub.