W3docs

Java CompletableFuture

Composez des calculs asynchrones avec CompletableFuture — thenApply, thenCompose, allOf, exceptionally et les pièges à éviter.

Future est un gestionnaire de résultat à usage unique : vous soumettez, vous attendez, vous lisez. Il ne peut pas enchaîner. Si vous voulez « faire A, puis avec le résultat de A faire B, puis combiner B avec C et passer à D » sans écrire une machine à états à la main, vous avez besoin de CompletableFuture — la refonte par Java 8 de l'idée de résultat asynchrone autour de la composition.

CompletableFuture<V> implémente Future<V>, donc toute l'ancienne API est toujours là. La nouveauté est l'API combinatoire : une trentaine de méthodes qui vous permettent de construire des graphes de flux de données de travaux asynchrones — appliquer des fonctions, exécuter des effets de bord, combiner plusieurs futures, récupérer des exceptions, gérer les délais — sans jamais bloquer un thread pour attendre un résultat intermédiaire.

Les méthodes de démarrage

En général, vous ne construisez pas un CompletableFuture directement. Vous démarrez un pipeline avec l'une de ces méthodes :

CompletableFuture<Integer> a = CompletableFuture.supplyAsync(() -> 42);
CompletableFuture<Void>    b = CompletableFuture.runAsync(() -> log("hello"));
CompletableFuture<String>  c = CompletableFuture.completedFuture("ready");
CompletableFuture<String>  d = CompletableFuture.failedFuture(new IOException("nope"));
Méthode de démarrageComportement
supplyAsync(Supplier)Exécute un Supplier sur le pool commun, retourne sa valeur
runAsync(Runnable)Exécute un Runnable sur le pool commun, sans valeur
completedFuture(v)Un future déjà résolu avec la valeur donnée
failedFuture(t)Un future déjà en échec avec le throwable donné

supplyAsync et runAsync ont des surcharges qui acceptent un Executor explicite. Vous voudrez presque toujours en passer un. La valeur par défaut est ForkJoinPool.commonPool() — un pool partagé dimensionné selon le nombre de vos CPUs, correct pour du travail CPU court mais désastreux si vous y mettez des I/O (un appel lent bloque un cœur pour tout le monde). Passez toujours un executor explicite pour les I/O ou les travaux de coût inconnu.

Enchaînement : thenApply, thenAccept, thenRun

Les combinateurs les plus simples transforment un future en un autre :

CompletableFuture<Integer> a = CompletableFuture.supplyAsync(() -> 42);

CompletableFuture<String>  b = a.thenApply(n -> "value is " + n);          // transform
CompletableFuture<Void>    c = a.thenAccept(n -> System.out.println(n));    // consume, no result
CompletableFuture<Void>    d = a.thenRun(() -> System.out.println("done")); // side-effect, ignore value
MéthodeType de lambdaRetourne
thenApplyFunction<T,U>CompletableFuture<U>
thenAcceptConsumer<T>CompletableFuture<Void>
thenRunRunnableCompletableFuture<Void>

Chaque méthode a trois variantes :

  • thenApply(fn) — s'exécute sur le thread qui complète l'étape précédente
  • thenApplyAsync(fn) — s'exécute sur le pool commun
  • thenApplyAsync(fn, executor) — s'exécute sur un executor spécifique

La forme non-Async est la plus rapide (pas de changement de thread) mais signifie que votre fn s'exécute sur le thread qui a complété l'étape précédente — possiblement le thread I/O que vous ne voulez pas occuper avec du travail CPU. Les formes *Async sont la valeur par défaut la plus sûre dans les pipelines hétérogènes.

thenCompose — aplatir un future de future

thenApply convient quand la fonction retourne une valeur simple. Quand elle retourne un autre CompletableFuture, vous ne voulez pas un CompletableFuture<CompletableFuture<V>> — vous voulez thenCompose :

CompletableFuture<User> user = lookupUser(id);
CompletableFuture<Profile> profile = user.thenCompose(u -> loadProfile(u.profileId()));
//                                          ^ Function<User, CompletableFuture<Profile>>

thenCompose est le flatMap pour les futures. Utilisez-le quand l'étape suivante est elle-même asynchrone ; utilisez thenApply quand elle ne l'est pas.

Combiner deux futures : thenCombine

Quand vous avez deux valeurs asynchrones indépendantes et souhaitez les combiner :

CompletableFuture<Integer> price   = fetchPrice(symbol);
CompletableFuture<Integer> shares  = fetchShares(account);
CompletableFuture<Integer> total   = price.thenCombine(shares, (p, s) -> p * s);

thenCombine attend les deux entrées, puis applique une BiFunction à leurs résultats. Les deux futures s'exécutent en parallèle — price et shares sont déjà en cours lorsque thenCombine est enregistré. Le combinateur s'exécute sur le thread qui complète en second.

La version « n'importe lequel », applyToEither, prend le premier résultat et ignore le second.

Plusieurs futures : allOf et anyOf

Quand le parallélisme porte sur une collection de futures :

List<CompletableFuture<String>> all = ids.stream()
    .map(this::fetchAsync)
    .toList();

CompletableFuture<Void> doneAll  = CompletableFuture.allOf(all.toArray(new CompletableFuture[0]));
CompletableFuture<Object> firstOne = CompletableFuture.anyOf(all.toArray(new CompletableFuture[0]));

allOf se complète quand toutes les entrées sont terminées. Il retourne CompletableFuture<Void> — pour récupérer la liste des résultats, vous devez utiliser thenApply et les extraire :

CompletableFuture<List<String>> results = doneAll.thenApply(v ->
    all.stream().map(CompletableFuture::join).toList());        // .join() never blocks here — they're all complete

anyOf retourne la valeur du premier futur à se compléter (en tant qu'Object — il n'y a aucun moyen d'exprimer « n'importe lequel de ces futures typés » avec un seul type de retour).

Gestion des erreurs : exceptionally et handle

Un CompletableFuture peut échouer (toute exception levée dans une étape produit un future en échec en aval). Les combinateurs qui récupèrent ou transforment :

CompletableFuture<String> safe = riskyAsync()
    .exceptionally(ex -> "fallback for: " + ex.getMessage());

CompletableFuture<String> either = riskyAsync()
    .handle((value, ex) -> ex == null ? value : "fallback");
MéthodeQuand elle s'exécuteCe qu'elle retourne
exceptionally(fn)Seulement en cas d'échec ; reçoit la causeValeur de récupération
handle(bi)Toujours ; reçoit (value, ex) (l'un est null)Valeur transformée
whenComplete(bi)Toujours ; reçoit (value, ex)Même future, effet de bord uniquement

exceptionally est le chemin simple « attraper et remplacer ». handle est plus général : « toujours s'exécuter, décider selon le résultat » — utile quand vous voulez journaliser chaque complétion indépendamment du succès.

orTimeout et completeOnTimeout

Java 9 a ajouté les délais directement dans l'API des futures :

CompletableFuture<String> withDeadline = riskyAsync()
    .orTimeout(2, TimeUnit.SECONDS);                  // completes exceptionally if not done in 2s

CompletableFuture<String> withDefault = riskyAsync()
    .completeOnTimeout("fallback", 2, TimeUnit.SECONDS);

Ceux-ci vous permettent d'exprimer des délais sans écrire votre propre surveillance. Ils utilisent des threads planifiés internes, donc ils sont peu coûteux à attacher.

Ne pas bloquer dans les étapes asynchrones

La plus grande erreur avec CompletableFuture : appeler .get() ou .join() à l'intérieur d'une étape Async. C'est un thread du pool d'executors assis à ne rien faire en attendant un autre thread du même pool — en charge, vous pouvez bloquer tout le pool dans un deadlock.

// WRONG — joining inside an async stage on the common pool
CompletableFuture.supplyAsync(() -> {
  Integer x = anotherFuture().join();                 // blocks a pool thread
  return x * 2;
});

// RIGHT — compose instead of join
anotherFuture().thenApply(x -> x * 2);

Si vous vous trouvez à atteindre .get() à l'intérieur d'une étape Async, c'est que vous vouliez thenCompose/thenApply à la place.

Utiliser votre propre executor

Le pool commun par défaut convient pour du travail CPU court. Pour les I/O ou tout ce qui pourrait bloquer, utilisez le vôtre :

ExecutorService io = Executors.newFixedThreadPool(50, namedFactory("io"));
ExecutorService cpu = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), namedFactory("cpu"));

CompletableFuture.supplyAsync(this::loadFromDb, io)
    .thenApplyAsync(this::transform, cpu)
    .thenAcceptAsync(this::sendToClient, io);

Chaque étape s'exécute sur le bon pool. Le pool commun reste disponible pour parallelStream et d'autres usages du framework. Ce mélange est au cœur d'un Java asynchrone bien conçu.

Un exemple concret : un petit pipeline asynchrone

Le programme ci-dessous récupère un « utilisateur » et un « profil » en parallèle, les combine, applique une limite de temps et récupère un chemin d'erreur.

java— editable, runs on the server

Ce qu'il faut retenir de l'exécution :

  • La section 1 a utilisé thenCombine sur deux récupérations indépendantes. Elles se sont exécutées en parallèlename (50 ms) et age (80 ms) étaient déjà en cours avant que le combinateur soit attaché. Le future combiné s'est complété peu après la fin du plus lent. C'est le parallélisme : un pipeline asynchrone n'attend pas chaque étape, il compose les étapes en un graphe.
  • La section 2 a utilisé thenCompose pour enchaîner des étapes où chaque étape est elle-même asynchrone. thenApply vous aurait donné CompletableFuture<CompletableFuture<String>> — inutilisable. thenCompose aplatit, à la manière dont flatMap le fait pour les streams et Optional.
  • La section 3 a utilisé allOf sur une liste, puis thenApply pour extraire les valeurs. allOf lui-même retourne Void ; la récolte des résultats est un stream séparé sur les futures (maintenant complétés) utilisant join(). Les appels join() ne bloquent pas ici parce que allOf s'est déjà complété.
  • La section 4 a montré exceptionally récupérant d'une tâche qui a levé une exception. Le future en amont a échoué ; le future en aval a retourné la chaîne de repli. Sans exceptionally (ou handle), l'échec se serait propagé jusqu'à .join() sous forme de CompletionException.
  • La section 5 a utilisé orTimeout pour appliquer une limite de 100 ms à une tâche de 500 ms. Le future s'est complété de manière exceptionnelle avec TimeoutException ; le join l'a relancé dans une CompletionException. C'est la bonne forme pour « je veux ce résultat, mais seulement s'il arrive assez vite. »
  • La section 6 a utilisé handle pour se brancher sur le succès ou l'échec en une seule étape. handle s'exécute toujours et reçoit (value, ex) — l'un est null. Utile quand vous voulez une fin de pipeline uniforme qu'importe si le travail a réussi ou non.

Et ensuite

Le prochain chapitre, Java Fork/Join, couvre le ForkJoinPool — le pool à vol de travail qui alimente les streams parallèles et le pool commun de CompletableFuture, et l'outil idéal pour le travail CPU de type diviser-pour-régner.

Pratique

Pratique
Vous écrivez `CompletableFuture.supplyAsync(() -> { Integer x = otherFuture().get(); return x * 2; })`. Dans le lambda, vous appelez `.get()` sur un autre future soumis au même pool par défaut. Quel est le risque ?
Vous écrivez `CompletableFuture.supplyAsync(() -> { Integer x = otherFuture().get(); return x * 2; })`. Dans le lambda, vous appelez `.get()` sur un autre future soumis au même pool par défaut. Quel est le risque ?
Was this page helpful?