CodeGym /Cours /JAVA 25 SELF /thenCompose + Executor personnalisé + délais d’expiration...

thenCompose + Executor personnalisé + délais d’expiration

JAVA 25 SELF
Niveau 55 , Leçon 4
Disponible

1. thenCompose vs. thenApply: différence et quand utiliser l’un ou l’autre

En programmation asynchrone en Java (via CompletableFuture), on doit souvent exécuter des chaînes d’actions. Pour cela, il existe deux méthodes similaires : thenApply et thenCompose. Mais elles fonctionnent différemment !

thenApply

La méthode thenApply s’utilise lorsque l’étape suivante est une simple transformation de valeur, sans lancer de nouvelles opérations asynchrones. Elle reçoit le résultat de l’étape précédente, le traite et renvoie une nouvelle valeur (pas un CompletableFuture).

Si vous connaissez le Stream API, thenApply se comporte un peu comme map : il prend le résultat, applique une fonction et renvoie la version transformée.

Exemple :

CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> "42");
CompletableFuture<Integer> lengthFuture = cf.thenApply(s -> s.length());
// lengthFuture contient 2 (longueur de la chaîne "42")

En bref, thenApply est un moyen de dire : « Quand le résultat est prêt, fais ceci avec lui ».

thenCompose

  • S’utilise quand l’étape suivante est une autre opération asynchrone (renvoie un CompletableFuture).
  • Permet de « déplier » des CompletableFuture imbriqués (analogue de flatMap).
  • Si vous utilisez thenApply avec une fonction asynchrone, vous obtiendrez un CompletableFuture<CompletableFuture<T>> — pas pratique !

Exemple :

CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> "user42");

// Supposons que nous devions, à partir du nom d’utilisateur, récupérer ses commandes (de manière asynchrone)
CompletableFuture<List<Order>> ordersFuture = cf.thenCompose(username -> fetchOrdersAsync(username));
// fetchOrdersAsync renvoie CompletableFuture<List<Order>>

Visuellement :

  • thenApply : CF<String>thenApply(s -> s.length())CF<Integer>
  • thenCompose : CF<User>thenCompose(u -> fetchOrdersAsync(u.id))CF<List<Order>>

Quand utiliser quoi ?

  • La fonction renvoie une valeur ordinaire — utilisez thenApply.
  • La fonction renvoie un CompletableFuture — utilisez thenCompose.

Exemple d’erreur :

cf.thenApply(username -> fetchOrdersAsync(username)); // Vous obtiendrez CF<CF<List<Order>>>
cf.thenCompose(username -> fetchOrdersAsync(username)); // Vous obtiendrez CF<List<Order>>

2. Gestion du pool de threads (Executor) : pourquoi et comment utiliser son propre Executor

Par défaut : ForkJoinPool.commonPool()

Lorsque vous écrivez CompletableFuture.supplyAsync(...) ou thenApplyAsync(...) sans préciser d’Executor, Java utilise le pool de threads commun — ForkJoinPool.commonPool(). C’est pratique, mais pas toujours adapté :

  • Si vous avez beaucoup d’opérations longues ou bloquantes (requêtes réseau, travail sur fichiers), le pool commun peut se saturer et toutes les tâches vont attendre.
  • Parfois il faut isoler des tâches avec des priorités différentes ou limiter le nombre de threads actifs simultanément.

Quand faut-il un Executor dédié ?

  • Opérations longues et bloquantes (par exemple, requêtes BD, requêtes HTTP, lecture de fichiers).
  • Isolation des tâches : pour que les tâches utilisateur ne perturbent pas les tâches système.
  • Limitation des ressources : par exemple, ne pas lancer plus de 10 téléchargements simultanés.

Comment créer son propre Executor

On utilise généralement ThreadPoolExecutor ou les fabriques de Executors :

ExecutorService myExecutor = Executors.newFixedThreadPool(10);

Comment utiliser votre Executor avec CompletableFuture

  • Dans les méthodes supplyAsync, runAsync, thenApplyAsync, thenComposeAsync et autres, vous pouvez passer en second argument votre Executor.

Exemples :

CompletableFuture<String> cf = CompletableFuture.supplyAsync(
    () -> loadDataFromNetwork(), myExecutor
);

cf.thenApplyAsync(data -> processData(data), myExecutor)
  .thenAcceptAsync(result -> System.out.println(result), myExecutor);

Important : si vous n’indiquez pas d’Executor, c’est ForkJoinPool.commonPool() qui sera utilisé.

Quand l’Executor par défaut suffit-il ?

  • Pour des tâches courtes, CPU-bound (calculs simples).
  • Quand le thread d’exécution importe peu.

3. Gestion des délais d’expiration : orTimeout et completeOnTimeout

Les opérations asynchrones peuvent se bloquer ou durer trop longtemps (par exemple si le serveur ne répond pas). Pour éviter d’attendre indéfiniment, CompletableFuture propose des méthodes pour gérer les délais d’expiration.

orTimeout

  • Termine le CompletableFuture avec l’exception TimeoutException si l’opération ne s’achève pas dans le temps imparti.
  • N’annule pas réellement la tâche en cours, mais la chaîne en aval recevra une erreur.

Syntaxe :

cf.orTimeout(3, TimeUnit.SECONDS)
  .exceptionally(ex -> {
      System.out.println("Délai dépassé: " + ex);
      return null;
  });

Exemple :

CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
    Thread.sleep(5000); // on simule une opération longue
    return "OK";
});

cf.orTimeout(2, TimeUnit.SECONDS)
  .exceptionally(ex -> {
      System.out.println("Erreur: " + ex);
      return "TIMEOUT";
  });

Résultat :

Au bout de 2 secondes, un TimeoutException sera levé et exceptionally traitera l’erreur.

completeOnTimeout

  • Termine le CompletableFuture avec la valeur indiquée si l’opération n’est pas terminée dans le délai.
  • Ne lève pas d’exception, mais renvoie une valeur de « secours ».

Syntaxe :

cf.completeOnTimeout("DEFAULT", 2, TimeUnit.SECONDS);

Exemple :

CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
    Thread.sleep(5000);
    return "OK";
});

cf.completeOnTimeout("TIMEOUT", 2, TimeUnit.SECONDS)
  .thenAccept(System.out::println); // Après 2 secondes, affichera "TIMEOUT"

Comparaison orTimeout vs completeOnTimeout

Méthode Que se passe-t-il en cas de dépassement de délai ? Comment est-ce traité ensuite ?
orTimeout
Se termine avec TimeoutException Peut être géré via exceptionally/handle
completeOnTimeout
Se termine avec la valeur fournie thenAccept/thenApply recevra cette valeur

4. Pratique : exemple avec thenCompose, un Executor personnalisé et un délai d’expiration

Tâche :

  • Obtenir l’utilisateur par id (asynchrone, avec latence).
  • Puis obtenir asynchronement la liste des commandes de l’utilisateur (également avec latence).
  • Utiliser un Executor personnalisé.
  • Ajouter un délai d’expiration pour la récupération des commandes.
import java.util.concurrent.*;
import java.util.*;

public class AsyncDemo {
    static ExecutorService ioExecutor = Executors.newFixedThreadPool(4);

    // Simulation de l’obtention asynchrone de l’utilisateur
    static CompletableFuture<String> fetchUserAsync(int userId) {
        return CompletableFuture.supplyAsync(() -> {
            sleep(1000);
            return "user" + userId;
        }, ioExecutor);
    }

    // Simulation de l’obtention asynchrone des commandes de l’utilisateur
    static CompletableFuture<List<String>> fetchOrdersAsync(String username) {
        return CompletableFuture.supplyAsync(() -> {
            sleep(3000); // Opération longue !
            return List.of("order1", "order2");
        }, ioExecutor);
    }

    static void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException ignored) {}
    }

    public static void main(String[] args) {
        fetchUserAsync(42)
            .thenCompose(username ->
                fetchOrdersAsync(username)
                    .orTimeout(2, TimeUnit.SECONDS) // Délai d’expiration pour la récupération des commandes
                    .exceptionally(ex -> {
                        System.out.println("Impossible d’obtenir les commandes: " + ex);
                        return List.of();
                    })
            )
            .thenAccept(orders -> System.out.println("Commandes: " + orders))
            .join(); // On attend la fin de toute la chaîne

        ioExecutor.shutdown();
    }
}

Ce qu’il se passe :

  • On obtient l’utilisateur (1 seconde).
  • On obtient les commandes (3 secondes, mais délai d’expiration de 2 secondes).
  • Si ce n’est pas terminé à temps — on intercepte TimeoutException et on renvoie une liste vide.
  • Tout fonctionne via l’Executor personnalisé.

Résultat :

Impossible d’obtenir les commandes: java.util.concurrent.TimeoutException
Commandes: []

Si vous réduisez la latence dans fetchOrdersAsync à 1_000 ms — vous verrez les commandes réelles.

5. Erreurs courantes et subtilités

Erreur n°1 : Utiliser thenApply au lieu de thenCompose pour des opérations asynchrones.
Si une fonction renvoie un CompletableFuture et que vous appliquez thenApply, vous obtiendrez le type imbriqué CompletableFuture<CompletableFuture<T>>. Cela complique la chaîne et ajoute des enveloppes inutiles. Solution : utilisez thenCompose pour « aplatir » le résultat en CompletableFuture<T>.

Erreur n°2 : Lancer des tâches longues ou d’E/S sans Executor dédié.
Par défaut les tâches s’exécutent dans ForkJoinPool.commonPool(). Si vous le surchargez, les latences vont augmenter et d’autres tâches de l’application peuvent ralentir. Solution : créez votre propre ExecutorService et passez-le à supplyAsync/thenApplyAsync.

Erreur n°3 : Penser que orTimeout annule la tâche.
orTimeout ne fait que terminer le CompletableFuture avec une exception due au délai, mais la tâche continue de tourner en arrière-plan. Solution : si vous devez l’arrêter, utilisez cancel(true) ou vos propres mécanismes d’interruption.

Erreur n°4 : Mauvaise compréhension de la portée du délai d’expiration.
orTimeout et completeOnTimeout ne s’appliquent qu’à une étape précise de la chaîne, et non à l’ensemble de la chaîne. Solution : si vous avez besoin d’un délai global, encapsulez toute la chaîne dans un CompletableFuture séparé et appliquez-lui le délai.

Erreur n°5 : Ne pas fermer ExecutorService.
Si, après l’exécution des tâches, vous n’appelez pas shutdown()/shutdownNow() sur l’ExecutorService, les threads continueront de tourner et le programme peut rester bloqué. Solution : fermez toujours l’ExecutorService dans un finally ou utilisez try-with-resources en Java 21+.

1
Mission
JAVA 25 SELF, niveau 55, leçon 4
Bloqué
Demande de statut : réponse instantanée avec secours
Demande de statut : réponse instantanée avec secours
1
Mission
JAVA 25 SELF, niveau 55, leçon 4
Bloqué
Panneau utilisateur : Accès rapide aux données avec gestion intelligente
Panneau utilisateur : Accès rapide aux données avec gestion intelligente
1
Étude/Quiz
Programmation asynchrone, niveau 55, leçon 4
Indisponible
Programmation asynchrone
Programmation asynchrone
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION