1. Lancement d’une tâche asynchrone : supplyAsync et runAsync
La manière la plus courante de lancer une tâche asynchrone est d’utiliser CompletableFuture.supplyAsync. Cette méthode prend une lambda ou une méthode qui renvoie un résultat. Par exemple, nous souhaitons simuler le chargement de données depuis un serveur :
import java.util.concurrent.CompletableFuture;
public class Main {
public static void main(String[] args) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// Simulation d'une opération longue (par exemple, le téléchargement d'un fichier)
sleep(1000);
return "Données du serveur";
});
System.out.println("Tâche lancée !");
// ... vous pouvez faire autre chose pendant que la tâche s'exécute
}
private static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException ignored) {}
}
}
runAsync : quand le résultat n’est pas nécessaire
Si votre tâche ne renvoie rien (par exemple, écrire simplement dans les logs, envoyer une notification), utilisez runAsync :
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
sleep(500);
System.out.println("Opération terminée !");
});
runAsync renvoie toujours CompletableFuture<Void>, puisqu’aucun résultat n’est attendu.
2. thenApply, thenAccept, thenRun : quelle différence ?
Quand une tâche asynchrone est terminée, on souhaite généralement faire quelque chose du résultat. Pour cela, on utilise des méthodes « gestionnaires » :
- thenApply — transforme le résultat et renvoie un nouveau résultat.
- thenAccept — consomme le résultat, ne renvoie rien (pour les effets de bord).
- thenRun — ne prend pas le résultat et ne renvoie rien (exécute simplement une action après l’achèvement de la tâche).
thenApply : traiter et transformer le résultat
Si vous devez transformer le résultat de la tâche précédente, utilisez thenApply. Par exemple, on a chargé une chaîne et l’on veut connaître sa longueur :
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Java");
CompletableFuture<Integer> lengthFuture = future.thenApply(s -> {
System.out.println("Calcul de la longueur de la chaîne...");
return s.length();
});
// lengthFuture contient maintenant Integer — la longueur de la chaîne "Java"
lengthFuture.thenAccept(len -> System.out.println("Longueur : " + len));
Ce qui se passe :
- future contient la chaîne "Java".
- thenApply transforme la chaîne en sa longueur (int).
- thenAccept affiche le résultat.
thenAccept : agir sur le résultat (sans rien renvoyer)
Si vous voulez simplement faire quelque chose du résultat (par exemple l’afficher), sans rien renvoyer — utilisez thenAccept :
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Bonjour, monde !");
future.thenAccept(result -> {
System.out.println("Résultat : " + result);
});
thenAccept — c’est comme un « consommateur » : il consomme le résultat et en fait quelque chose d’utile.
thenRun : action sans résultat
Si vous voulez simplement exécuter une action après la fin de la tâche, mais que le résultat ne vous intéresse pas, utilisez thenRun :
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Terminé !");
future.thenRun(() -> {
System.out.println("Chargement terminé !");
});
Attention : à l’intérieur de thenRun, vous ne pouvez pas accéder au résultat de la tâche précédente — il est simplement ignoré.
3. Chaînes d’appels : construire un pipeline de tâches
La plus grande force de CompletableFuture — c’est la capacité à construire des chaînes de calculs. Chaque méthode (thenApply, thenAccept, thenRun) renvoie un nouveau CompletableFuture auquel on peut à nouveau ajouter un gestionnaire.
Exemple : traitement en plusieurs étapes
Améliorons notre application : chargeons des données, transformons-les, affichons le résultat et écrivons dans les logs que tout est terminé.
CompletableFuture.supplyAsync(() -> {
System.out.println("Étape 1 : Chargement des données...");
sleep(500);
return "Java";
})
.thenApply(data -> {
System.out.println("Étape 2 : Transformation des données...");
return data.toUpperCase();
})
.thenAccept(result -> {
System.out.println("Étape 3 : Affichage du résultat : " + result);
})
.thenRun(() -> {
System.out.println("Étape 4 : Tout est terminé !");
});
Sortie dans la console :
Étape 1 : Chargement des données...
Étape 2 : Transformation des données...
Étape 3 : Affichage du résultat : JAVA
Étape 4 : Tout est terminé !
À noter :
Chaque étape suivante ne commence qu’après la fin de la précédente. Cela permet de construire de véritables « pipelines » de traitement des données.
4. Variantes asynchrones : thenApplyAsync, thenAcceptAsync, thenRunAsync
Par défaut, les gestionnaires (thenApply, thenAccept, thenRun) s’exécutent dans le même thread que celui où la tâche précédente s’est terminée. Parfois, ce n’est pas idéal — si le traitement est lourd, mieux vaut l’exécuter dans un thread séparé.
Pour cela, des versions asynchrones existent :
- thenApplyAsync
- thenAcceptAsync
- thenRunAsync
Quelle est la différence ?
- Sans Async : le gestionnaire peut s’exécuter dans le même thread que la tâche précédente (par exemple, si la tâche s’est terminée dans le ForkJoinPool, le gestionnaire s’y exécutera aussi).
- Avec Async : le gestionnaire s’exécutera garanti dans un autre thread du ForkJoinPool (ou de votre Executor).
Exemple : comparer un gestionnaire normal et un gestionnaire asynchrone
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
System.out.println("Chargement... [" + Thread.currentThread().getName() + "]");
return "Hello";
});
future.thenApply(result -> {
System.out.println("thenApply: [" + Thread.currentThread().getName() + "]");
return result + " World";
});
future.thenApplyAsync(result -> {
System.out.println("thenApplyAsync: [" + Thread.currentThread().getName() + "]");
return result + " Async World";
});
Sortie typique :
Chargement... [ForkJoinPool.commonPool-worker-1]
thenApply: [ForkJoinPool.commonPool-worker-1]
thenApplyAsync: [ForkJoinPool.commonPool-worker-2]
Conclusion :
Le gestionnaire asynchrone s’exécute dans un autre thread.
Quand utiliser les méthodes Async ?
- Si le traitement est gourmand en ressources (par exemple, calculs lourds, travail réseau).
- Si vous ne voulez pas bloquer le thread dans lequel la tâche précédente s’est terminée.
- Si vous voulez gérer explicitement les threads (par exemple, passer votre Executor comme deuxième argument).
5. Points utiles
Tableau : comparaison des méthodes thenApply, thenAccept, thenRun
| Méthode | Utilise le résultat ? | Renvoie une valeur ? | À quoi sert |
|---|---|---|---|
|
Oui | Oui | Transformation du résultat |
|
Oui | Non | Effets de bord (affichage, journalisation) |
|
Non | Non | Simple action après l’achèvement de la tâche |
|
Oui | Oui | Idem, mais dans un autre thread |
|
Oui | Non | Idem, mais dans un autre thread |
|
Non | Non | Idem, mais dans un autre thread |
Question : comment construire de longues chaînes ?
Vous pouvez appeler les méthodes les unes après les autres, comme des briques LEGO :
CompletableFuture.supplyAsync(() -> "42")
.thenApply(Integer::parseInt)
.thenApply(x -> x * 2)
.thenAccept(x -> System.out.println("Résultat : " + x));
Sortie :
Résultat : 84
Chaque étape suivante reçoit le résultat de la précédente, peut le modifier ou simplement l’utiliser.
6. Erreurs courantes avec thenApply, thenAccept, thenRun
Erreur n° 1 : confusion sur les types de retour.
thenApply doit renvoyer une valeur qui ira plus loin dans la chaîne. Si vous utilisez par erreur thenApply sans renvoyer de résultat, l’opération suivante recevra null (ou ne compilera pas du tout). Pour les effets de bord, utilisez thenAccept ou thenRun.
Erreur n° 2 : tenter d’utiliser le résultat dans thenRun.
À l’intérieur de thenRun, il n’y a pas d’accès au résultat de la tâche précédente. Si vous voulez utiliser le résultat, choisissez thenApply ou thenAccept.
Erreur n° 3 : blocage du thread principal.
Si vous appelez get() ou join() dans le thread principal, vous perdez tous les avantages de l’asynchronisme : le thread attendra la fin de la tâche, comme dans un vieux code synchrone. Il vaut mieux utiliser des chaînes non bloquantes et des callbacks.
Erreur n° 4 : absence de gestion des erreurs.
Si une exception survient dans la chaîne et que vous n’ajoutez pas de gestionnaire (exceptionally, handle, whenComplete), elle « se perdra » et la tâche pourra se terminer avec une erreur que vous ne verrez pas. Gérez toujours les erreurs dans les chaînes.
Erreur n° 5 : exécution inattendue dans un autre thread.
Les méthodes asynchrones (thenApplyAsync et autres) peuvent s’exécuter dans un autre thread. Si vous accédez à des variables non protégées pour l’accès concurrent, des conditions de concurrence peuvent apparaître.
GO TO FULL VERSION