Guide To CompletableFuture

Guide de CompletableFuture

1. introduction

Cet article est un guide des fonctionnalités et des cas d'utilisation de la classeCompletableFuture - présentée comme une amélioration de l'API Java 8 Concurrency.

Lectures complémentaires:

Runnable vs. Callable en Java

Découvrez la différence entre les interfaces Runnable et Callable en Java.

Read more

Guide de java.util.concurrent.Future

Un guide sur java.util.concurrent.Future avec un aperçu de ses différentes implémentations

Read more

2. Calcul asynchrone en Java

Le calcul asynchrone est difficile à raisonner. Habituellement, nous voulons considérer tout calcul comme une série d’étapes. Mais en cas de calcul asynchrone,actions represented as callbacks tend to be either scattered across the code or deeply nested inside each other. La situation empire encore lorsque nous devons gérer les erreurs pouvant survenir au cours d’une des étapes.

L'interfaceFuture a été ajoutée dans Java 5 pour servir de résultat d'un calcul asynchrone, mais elle ne disposait d'aucune méthode pour combiner ces calculs ou gérer d'éventuelles erreurs.

In Java 8, the CompletableFuture class was introduced. En plus de l'interfaceFuture, il a également implémenté l'interfaceCompletionStage. Cette interface définit le contrat pour une étape de calcul asynchrone pouvant être combinée avec d'autres étapes.

CompletableFuture est à la fois un bloc de construction et un framework avecabout 50 different methods for composing, combining, executing asynchronous computation steps and handling errors.

Une API aussi volumineuse peut être accablante, mais elle s’inscrit principalement dans plusieurs cas d’utilisation clairs et distincts.

3. Utilisation deCompletableFuture comme simpleFuture

Tout d'abord, la classeCompletableFuture implémente l'interfaceFuture, donc vous pouvezuse it as a Future implementation, but with additional completion logic.

Par exemple, vous pouvez créer une instance de cette classe avec un constructeur sans argument pour représenter un résultat futur, la distribuer aux consommateurs et la compléter ultérieurement à l'aide de la méthodecomplete. Les consommateurs peuvent utiliser la méthodeget pour bloquer le thread actuel jusqu'à ce que ce résultat soit fourni.

Dans l'exemple ci-dessous, nous avons une méthode qui crée une instance deCompletableFuture, puis effectue un certain calcul dans un autre thread et renvoie lesFuture immédiatement.

Lorsque le calcul est terminé, la méthode complète lesFuture en fournissant le résultat à la méthodecomplete:

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

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

    return completableFuture;
}

Pour faire tourner le calcul, nous utilisons l'APIExecutor qui est décrite dans l'article“Introduction to Thread Pools in Java”, mais cette méthode de création et de réalisation d'unCompletableFuture peut être utilisée avec n'importe quel mécanisme de concurrence ou API y compris les fils bruts.

Notez quethe calculateAsync method returns a Future instance.

Nous appelons simplement la méthode, recevons l’instanceFuture et appelons la méthodeget dessus lorsque nous sommes prêts à bloquer le résultat.

Notez également que la méthodeget lève des exceptions vérifiées, à savoirExecutionException (encapsulant une exception survenue lors d'un calcul) etInterruptedException (une exception signifiant qu'un thread exécutant une méthode a été interrompu) :

Future completableFuture = calculateAsync();

// ...

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

If you already know the result of a computation, vous pouvez utiliser la méthode statiquecompletedFuture avec un argument qui représente un résultat de ce calcul. Ensuite, la méthodeget desFuture ne se bloquera jamais, renvoyant immédiatement ce résultat à la place.

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

// ...

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

Comme scénario alternatif, vous voudrez peut-êtrecancel the execution of a Future.

Supposons que nous n’ayons pas réussi à trouver un résultat et que nous décidions d’annuler complètement une exécution asynchrone. Cela peut être fait avec la méthodecancel deFuture. Cette méthode reçoit un argumentbooleanmayInterruptIfRunning, mais dans le cas deCompletableFuture, elle n'a aucun effet, car les interruptions ne sont pas utilisées pour contrôler le traitement deCompletableFuture.

Voici une version modifiée de la méthode asynchrone:

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

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

    return completableFuture;
}

Lorsque nous bloquons le résultat en utilisant la méthodeFuture.get(), cela lanceCancellationException si le futur est annulé:

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

4. CompletableFuture avec logique de calcul encapsulée

Le code ci-dessus nous permet de choisir n’importe quel mécanisme d’exécution simultanée, mais qu’en est-il si nous voulons ignorer cette passe-partout et simplement exécuter du code de manière asynchrone?

Les méthodes statiquesrunAsync etsupplyAsync nous permettent de créer une instanceCompletableFuture à partir des types fonctionnelsRunnable etSupplier en conséquence.

LesRunnable etSupplier sont des interfaces fonctionnelles qui permettent de transmettre leurs instances en tant qu'expressions lambda grâce à la nouvelle fonctionnalité Java 8.

L'interfaceRunnable est la même ancienne interface que celle utilisée dans les threads et elle ne permet pas de renvoyer une valeur.

L'interfaceSupplier est une interface fonctionnelle générique avec une méthode unique qui n'a pas d'arguments et renvoie une valeur de type paramétré.

Cela permet deprovide an instance of the Supplier as a lambda expression that does the calculation and returns the result. C'est aussi simple que:

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

// ...

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

5. Traitement des résultats de calculs asynchrones

La manière la plus générique de traiter le résultat d'un calcul consiste à l'envoyer à une fonction. La méthodethenApply fait exactement cela: accepte une instance deFunction, l'utilise pour traiter le résultat et renvoie unFuture qui contient une valeur retournée par une fonction:

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

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

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

Si vous n'avez pas besoin de renvoyer une valeur dans la chaîneFuture, vous pouvez utiliser une instance de l'interface fonctionnelle deConsumer. Sa méthode unique prend un paramètre et renvoievoid.

Il existe une méthode pour ce cas d'utilisation dans leCompletableFuture - la méthodethenAccept reçoit unConsumer et lui transmet le résultat du calcul. L'appel final defuture.get() renvoie une instance du typeVoid.

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

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

future.get();

Enfin, si vous n'avez pas besoin de la valeur du calcul ni que vous souhaitez renvoyer une valeur à la fin de la chaîne, vous pouvez passer un lambdaRunnable à la méthodethenRun. Dans l'exemple suivant, après l'appel de la méthodefuture.get(), nous imprimons simplement une ligne dans la console:

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

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

future.get();

6. Combiner des contrats à terme

La meilleure partie de l'APICompletableFuture est leability to combine CompletableFuture instances in a chain of computation steps.

Le résultat de ce chaînage est lui-même unCompletableFuture qui permet un chaînage et une combinaison supplémentaires. Cette approche est omniprésente dans les langages fonctionnels et est souvent appelée modèle de conception monadique.

Dans l'exemple suivant, nous utilisons la méthodethenCompose pour enchaîner deuxFutures séquentiellement.

Notez que cette méthode prend une fonction qui renvoie une instance deCompletableFuture. L'argument de cette fonction est le résultat de l'étape de calcul précédente. Cela nous permet d'utiliser cette valeur dans le lambda suivant deCompletableFuture:

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

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

La méthodethenCompose avecthenApply implémente les blocs de construction de base du modèle monadique. Ils sont étroitement liés aux méthodesmap etflatMap des classesStream etOptional également disponibles en Java 8.

Les deux méthodes reçoivent une fonction et l'appliquent au résultat du calcul, mais la méthodethenCompose (flatMap)receives a function that returns another object of the same type. Cette structure fonctionnelle permet de composer les instances de ces classes sous forme de blocs de construction.

Si vous voulez exécuter deuxFutures indépendants et faire quelque chose avec leurs résultats, utilisez la méthodethenCombine qui accepte unFuture et unFunction avec deux arguments pour traiter les deux résultats:

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

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

Un cas plus simple est celui où vous voulez faire quelque chose avec deux résultatsFutures, mais vous n’avez pas besoin de transmettre une valeur résultante dans une chaîneFuture. La méthodethenAcceptBoth est là pour vous aider:

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

7. Différence entrethenApply() etthenCompose()

Dans nos sections précédentes, nous avons montré des exemples concernantthenApply() etthenCompose(). Les deux API permettent de chaîner différents appelsCompletableFuture mais l'utilisation de ces 2 fonctions est différente.

7.1. thenApply()

This method is used for working with a result of the previous call. Cependant, un point clé à retenir est que le type de retour sera combiné de tous les appels.

Donc, cette méthode est utile lorsque nous voulons transformer le résultat d'un scallCompletableFuture :

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

7.2. thenCompose()

La méthodethenCompose() est similaire àthenApply() en ce que les deux renvoient une nouvelle étape d'achèvement. Cependant,thenCompose() uses the previous stage as the argument. Il s'aplatira et retournera unFuture avec le résultat directement, plutôt qu'un futur imbriqué comme nous l'avons observé dansthenApply():

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

Donc, si l’idée est d’enchaîner les méthodesCompletableFuture, il est préférable d’utiliserthenCompose().

Notez également que la différence entre ces deux méthodes est analogue àhttps://www.example.com/java-difference-map-and-flatmap.

8. Exécution de plusieursFutures en parallèle

Lorsque nous devons exécuter plusieursFutures en parallèle, nous voulons généralement attendre qu'ils s'exécutent tous, puis traiter leurs résultats combinés.

La méthode statique deCompletableFuture.allOf permet d'attendre la fin de tous lesFutures fournis en 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());

Notez que le type de retour desCompletableFuture.allOf() est unCompletableFuture<Void>. La limitation de cette méthode est qu'elle ne renvoie pas les résultats combinés de tous lesFutures. Au lieu de cela, vous devez obtenir manuellement les résultats deFutures. Heureusement, la méthodeCompletableFuture.join() et l'API Java 8 Streams simplifient les choses:

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

assertEquals("Hello Beautiful World", combined);

La méthodeCompletableFuture.join() est similaire à la méthodeget, mais elle lève une exception non vérifiée au cas où leFuture ne se termine pas normalement. Cela permet de l'utiliser comme référence de méthode dans la méthodeStream.map().

9. Gestion des erreurs

Pour la gestion des erreurs dans une chaîne d'étapes de calcul asynchrones, l'idiome dethrow/catcha dû être adapté de la même manière.

Au lieu d'attraper une exception dans un bloc syntaxique, la classeCompletableFuture vous permet de la gérer dans une méthode spécialehandle. Cette méthode reçoit deux paramètres: le résultat d'un calcul (s'il s'est terminé avec succès) et l'exception levée (si une étape de calcul ne s'est pas terminée normalement).

Dans l'exemple suivant, nous utilisons la méthodehandle pour fournir une valeur par défaut lorsque le calcul asynchrone d'un message d'accueil s'est terminé avec une erreur car aucun nom n'a été fourni:

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());

Comme scénario alternatif, supposons que nous voulions compléter manuellement lesFuture avec une valeur, comme dans le premier exemple, mais aussi avoir la possibilité de le compléter avec une exception. La méthodecompleteExceptionally est destinée à cela. La méthodecompletableFuture.get() de l'exemple suivant renvoie unExecutionException avec unRuntimeException comme cause:

CompletableFuture completableFuture = new CompletableFuture<>();

// ...

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

// ...

completableFuture.get(); // ExecutionException

Dans l'exemple ci-dessus, nous aurions pu gérer l'exception avec la méthodehandle de manière asynchrone, mais avec la méthodeget, nous pouvons utiliser une approche plus typique d'un traitement d'exception synchrone.

10. Méthodes asynchrones

La plupart des méthodes de l'API fluent de la classeCompletableFuture ont deux variantes supplémentaires avec le suffixeAsync. Ces méthodes sont généralement destinées auxrunning a corresponding step of execution in another thread.

Les méthodes sans le suffixeAsync exécutent la prochaine étape d'exécution en utilisant un thread appelant. La méthodeAsync sans l'argumentExecutor exécute une étape en utilisant l'implémentation commune du poolfork/join deExecutor accessible avec la méthodeForkJoinPool.commonPool(). La méthodeAsync avec un argumentExecutor exécute une étape en utilisant lesExecutor passés.

Voici un exemple modifié qui traite le résultat d'un calcul avec une instanceFunction. La seule différence visible est la méthodethenApplyAsync. Mais sous le capot, l'application d'une fonction est encapsulée dans une instance deForkJoinTask (pour plus d'informations sur le frameworkfork/join, voir l'article“Guide to the Fork/Join Framework in Java”). Cela permet de paralléliser davantage vos calculs et d'utiliser les ressources système plus efficacement.

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

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

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

11. API du JDK 9CompletableFuture

Dans Java 9, l'APICompletableFuture a été encore améliorée avec les modifications suivantes:

  • Nouvelles méthodes d'usine ajoutées

  • Prise en charge des retards et des délais

  • Prise en charge améliorée du sous-classement.

De nouvelles API d'instances ont été introduites:

  • Executor defaultExecutor ()

  • CompletableFuture nouveauIncompleteFuture ()

  • Copie CompletableFuture ()

  • CompletionStage minimalCompletionStage ()

  • CompletableFuture completeAsync (Supplier fournisseur, exécuteur exécuteur)

  • CompletableFuture completeAsync (Supplier fournisseur)

  • CompletableFuture ouTimeout (long timeout, unité TimeUnit)

  • CompletableFuture completeOnTimeout (valeur T, long timeout, unité TimeUnit)

Nous avons aussi maintenant quelques méthodes d’utilité statique:

  • Executor delayExecutor (long delay, unité TimeUnit, Executor executor)

  • Executor delayExecutor (long delay, unité TimeUnit)

  • CompletionStage completedStage (valeur U)

  • CompletionStage échoué

  • CompletableFuture failedFuture (Throwable ex)

Enfin, pour faire face au délai d'attente, Java 9 a introduit deux nouvelles fonctions supplémentaires:

  • orTimeout ()

  • completeOnTimeout ()

Voici l'article détaillé pour en savoir plus:Java 9 CompletableFuture API Improvements.

12. Conclusion

Dans cet article, nous avons décrit les méthodes et les cas d’utilisation typiques de la classeCompletableFuture.

Le code source de l'article est disponibleover on GitHub.