CodeGym /Cours /JAVA 25 SELF /Combiner CompletableFuture : thenCombine, allOf, any...

Combiner CompletableFuture : thenCombine, allOf, anyOf

JAVA 25 SELF
Niveau 55 , Leçon 2
Disponible

1. Combiner CompletableFuture

Dans la vie réelle, il est rare que tout vienne d’une seule source : on peut charger en parallèle le profil d’un utilisateur et ses commandes, combiner des données provenant de deux microservices — et parfois on souhaite simplement traiter la première réponse reçue. L’approche synchrone oblige à attendre l’un après l’autre ; la classe CompletableFuture permet de tout lancer en même temps et de combiner les résultats élégamment. Elle fournit pour cela des méthodes dédiées : thenCombine, allOf, anyOf. Voyons chacune d’elles à tour de rôle.

Tâches parallèles : deux requêtes asynchrones

À quoi cela ressemblerait-il en mode synchrone :

String name = loadUserName();    // long
int balance = loadUserBalance(); // long
System.out.println("Nom: " + name + ", Solde: " + balance);

Problème : le deuxième appel ne démarre qu’après la fin du premier.

Approche asynchrone

Avec CompletableFuture, les deux tâches peuvent être lancées simultanément :

CompletableFuture<String> nameFuture = CompletableFuture.supplyAsync(() -> loadUserName());
CompletableFuture<Integer> balanceFuture = CompletableFuture.supplyAsync(() -> loadUserBalance());

Mais comment obtenir maintenant les deux résultats et les traiter ensemble ? C’est le rôle de thenCombine.

2. thenCombine : combiner les résultats de deux tâches

La méthode thenCombine permet de combiner deux CompletableFuture et d’exécuter une action lorsque les deux tâches sont terminées. Elle renvoie un nouveau CompletableFuture contenant le résultat de la combinaison.

Signature :

<A, B, C> CompletableFuture<C> thenCombine(
    CompletionStage<? extends B> other,
    BiFunction<? super A, ? super B, ? extends C> fn
)
  • A — type du résultat du premier future,
  • B — type du second,
  • C — type du résultat combiné.

Exemple

CompletableFuture<String> nameFuture = CompletableFuture.supplyAsync(() -> loadUserName());
CompletableFuture<Integer> balanceFuture = CompletableFuture.supplyAsync(() -> loadUserBalance());

CompletableFuture<String> resultFuture = nameFuture.thenCombine(
    balanceFuture,
    (name, balance) -> "Nom: " + name + ", Solde: " + balance
);

resultFuture.thenAccept(System.out::println);

Fonctionnement :

  • Les deux futures démarrent en parallèle.
  • Dès qu’ils sont tous les deux terminés, la fonction (name, balance) -> ... est appelée.
  • Le future résultant contient la chaîne de résultat.

Mini-exemple avec des nombres

CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> 2);
CompletableFuture<Integer> f2 = CompletableFuture.supplyAsync(() -> 3);
CompletableFuture<Integer> sum = f1.thenCombine(f2, Integer::sum);

sum.thenAccept(result -> System.out.println("Somme: " + result));

Sortie :

Somme: 5

Variante asynchrone

Si la combinaison est une opération lourde, utilisez thenCombineAsync :

f1.thenCombineAsync(f2, (a, b) -> a * b);

3. allOf : quand il y a beaucoup de tâches

Et si nous n’avions pas deux tâches, mais une dizaine ? Par exemple, nous voulons charger en parallèle les données de dix utilisateurs à la fois. Pour cela, il existe la méthode CompletableFuture.allOf.

Description

CompletableFuture.allOf(f1, f2, ..., fn) renvoie un nouveau future qui se termine lorsque toutes les tâches passées en paramètre sont terminées. Mais il y a un point important : ce future ne contient pas de résultat — son type est toujours CompletableFuture<Void>. Pour obtenir les résultats, il faut les « récupérer » séparément à partir des futures d’origine.

Exemple

CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> "Premier");
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> "Deuxième");

CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2);

all.thenRun(() -> {
    // Toutes les tâches sont terminées!
    String s1 = f1.join(); // join() — comme get(), mais lève une exception non vérifiée
    String s2 = f2.join();
    System.out.println(s1 + " & " + s2);
});

Sortie :

Premier & Deuxième

Exemple avec un tableau de tâches

List<CompletableFuture<String>> futures = new ArrayList<>();
for (int i = 0; i < 5; i++) {
    int id = i;
    futures.add(CompletableFuture.supplyAsync(() -> "Utilisateur " + id));
}

CompletableFuture<Void> all = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));

all.thenRun(() -> {
    for (CompletableFuture<String> f : futures) {
        System.out.println(f.join());
    }
});

Ce qui se passe :

  • Toutes les tâches démarrent en parallèle.
  • allOf se termine lorsque toutes les tâches le sont.
  • Dans le bloc thenRun, nous pouvons obtenir les résultats via join().

Schéma visuel

[Future1]   \
[Future2] ----> [allOf] ---> thenRun
[Future3]   /

4. anyOf : attendre la première tâche terminée

Parfois, il ne faut pas attendre tout le monde, mais récupérer le résultat de la tâche la plus rapide. Par exemple, nous interrogeons deux serveurs — on utilise les données de celui qui répond en premier. Pour cela, il existe CompletableFuture.anyOf.

Description

CompletableFuture.anyOf(f1, f2, ..., fn) renvoie un future qui se termine lorsque n’importe laquelle des tâches données est terminée. Le type du résultat est CompletableFuture<Object>, car les types des tâches peuvent différer.

Exemple

CompletableFuture<String> fast = CompletableFuture.supplyAsync(() -> {
    sleep(500);
    return "Serveur rapide";
});
CompletableFuture<String> slow = CompletableFuture.supplyAsync(() -> {
    sleep(2000);
    return "Serveur lent";
});

CompletableFuture<Object> any = CompletableFuture.anyOf(fast, slow);

any.thenAccept(result -> System.out.println("Reçu: " + result));

Sortie :

Reçu: Serveur rapide

Exemple avec des types différents
On peut combiner des tâches de types différents, mais le résultat sera alors de type Object, et un cast explicite sera nécessaire.

5. Subtilités utiles

Recommandations

  • allOf ne renvoie pas un tableau de résultats. Il faut conserver les futures d’origine pour récupérer leurs valeurs via join() ou get().
  • anyOf renvoie le premier résultat terminé, mais de type Object. Si toutes les tâches sont du même type, un cast est possible.
  • Si l’une des tâches dans allOf se termine par une erreur, le future final se termine aussi par une erreur.
  • Pour thenCombine, les deux tâches doivent se terminer avec succès, sinon — exception.

Tableau comparatif des méthodes

Méthode Quand l’utiliser Type de résultat
thenCombine
Il faut combiner les résultats de deux tâches Résultat de la combinaison
allOf
Il faut attendre la fin de toutes les tâches
Void
anyOf
Il faut attendre la fin de n’importe quelle tâche Object (résultat de la première tâche)

6. Erreurs courantes lors de la combinaison de CompletableFuture

Erreur n°1 : Attendre le résultat via get()/join() dans le thread principal.
Si vous écrivez du code asynchrone mais qu’à la fin vous appelez quand même get() ou join(), vous bloquez le thread et perdez tous les avantages de l’asynchronisme. Il vaut mieux utiliser thenAccept/thenRun pour traiter le résultat sans blocage.

Erreur n°2 : Ne pas conserver les références aux futures d’origine lors de l’utilisation de allOf.
Si vous avez appelé CompletableFuture.allOf(f1, f2, f3) mais n’avez pas sauvegardé f1, f2, f3, vous ne pourrez pas récupérer leurs résultats. allOf renvoie uniquement Void !

Erreur n°3 : Ne pas gérer les erreurs dans la chaîne.
Si l’une des tâches se termine par une erreur, l’ensemble allOf ou thenCombine se termine aussi par une erreur. Utilisez les méthodes de traitement des erreurs (exceptionally, handle, whenComplete) pour ne pas manquer les exceptions.

Erreur n°4 : Incompatibilité des types avec anyOf.
anyOf renvoie un Object. Si vos futures renvoient des types différents, il faudra déterminer ce qui est arrivé en premier. Il est préférable d’utiliser des types de tâches identiques lorsque c’est possible.

Erreur n°5 : Chaînes trop complexes sans commentaires.
Quand le code devient volumineux, les chaînes de futures peuvent se transformer en « spaghetti ». N’hésitez pas à découper la chaîne en variables distinctes et à commenter les étapes.

Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION