W3docs

Java Consumer et Supplier

Les interfaces fonctionnelles Consumer (effets de bord) et Supplier (production de valeurs) en Java, avec exemples et spécialisations.

Consumer<T> et Supplier<T> sont les deux interfaces fonctionnelles correspondant aux coins non purs de la taxonomie à quatre coins :

  • Consumer<T> prend une valeur et ne retourne rien — son rôle est l'effet de bord (afficher, journaliser, écrire, ajouter dans une collection).
  • Supplier<T> ne prend rien et retourne une valeur — son rôle est de produire un T de façon paresseuse, à la demande (valeurs par défaut, fabriques, aléatoire).

Les deux complètent les chapitres sur Function/Predicate qui les ont précédés : ceux-ci retournaient une valeur à partir d'une valeur, alors que ces interfaces entrent et sortent du monde environnant. Ce chapitre couvre les deux interfaces car leurs API sont réduites et leurs points d'utilisation se recoupent.

Consumer<T>

@FunctionalInterface
public interface Consumer<T> {
  void accept(T t);                                         // the only abstract method

  default Consumer<T> andThen(Consumer<? super T> after);
}

Un Consumer signifie « faire quelque chose avec ce T ». La méthode SAM est accept. La seule méthode par défaut andThen enchaîne des consumers afin qu'ils s'exécutent en séquence sur la même entrée :

Consumer<String> log    = System.out::println;
Consumer<String> store  = audit::record;
Consumer<String> both   = log.andThen(store);
both.accept("hello");    // prints "hello", then audit.record("hello")

andThen ne court-circuite pas si le premier consumer lève une exception — il laisse l'exception se propager, et le second consumer ne s'exécute jamais. C'est la même sémantique qu'écrire les deux appels dans un bloc sans try : l'échec interrompt la séquence.

Consumer<T> apparaît

list.forEach(System.out::println);                 // Iterable.forEach(Consumer)
stream.forEach(System.out::println);               // Stream.forEach
optional.ifPresent(name -> log.info(name));         // Optional.ifPresent
queue.peek(System.out::println);                    // not a Consumer call, but the shape is the same

Partout où le JDK dit « faire quelque chose avec chaque élément », le paramètre est un Consumer<T> ou un BiConsumer<K, V> pour les cas à deux arguments (notamment Map.forEach((k, v) -> ...)).

BiConsumer<T, U>

La variante à deux arguments :

BiConsumer<String, Integer> show = (k, v) -> System.out.println(k + " => " + v);
Map<String, Integer> scores = Map.of("alice", 1, "bob", 2);
scores.forEach(show);

BiConsumer dispose du même andThen par défaut. Il n'existe pas de BiSupplier — un Supplier à deux arguments serait simplement un BiFunction<T, U, R>.

Spécialisations primitives — IntConsumer, LongConsumer, DoubleConsumer

IntConsumer    printInt = System.out::println;       // accepts int, no boxing
LongConsumer   tally    = n -> total += n;
DoubleConsumer record   = d -> samples.add(d);

Même sémantique andThen. IntStream.forEach accepte un IntConsumer, ce qui permet à un flux primitif d'appeler votre lambda sans boxing.

Il existe également ObjIntConsumer<T>, ObjLongConsumer<T>, ObjDoubleConsumer<T> pour le cas où un argument est un objet et l'autre est un primitif — Stream.collect(Supplier, BiConsumer, BiConsumer) et ses cousins primitifs les utilisent.

Supplier<T>

@FunctionalInterface
public interface Supplier<T> {
  T get();                                                   // the only abstract method
}

C'est toute l'interface — pas de méthodes par défaut, pas d'andThen, pas de composition. La raison est qu'un Supplier a la forme la plus simple possible : zéro entrée, une sortie, et la seule chose que l'on peut faire avec est appeler get().

Supplier<List<String>> empty = ArrayList::new;
Supplier<UUID>         id    = UUID::randomUUID;
Supplier<String>       expensive = () -> loadFromDb();

Supplier<T> apparaît

Supplier est la façon dont le JDK exprime la paresse — « donne-moi cette valeur, mais seulement quand j'en ai besoin » :

opt.orElseGet(() -> loadDefault());                                  // lazy default
Objects.requireNonNullElseGet(value, () -> sentinel);                // lazy default for null
Stream.generate(() -> Math.random()).limit(5);                        // infinite stream of supplied values
logger.debug("expensive: {}", () -> serialiseGraph(state));           // lazy log argument
CompletableFuture.supplyAsync(() -> compute());                        // run the supplier on another thread

Chaque fois qu'un Supplier<T> apparaît dans le JDK, le contrat est « cette valeur pourrait ne jamais être nécessaire ». Optional.orElseGet n'appelle get() que lorsque l'optional est vide ; Stream.generate ne l'appelle que lorsque l'élément suivant est demandé. Cette paresse est tout l'intérêt — un argument T ordinaire aurait déjà été calculé au moment où la méthode était invoquée.

Spécialisations primitives — IntSupplier, LongSupplier, DoubleSupplier, BooleanSupplier

IntSupplier     count   = () -> counter.getAndIncrement();
DoubleSupplier  random  = Math::random;
BooleanSupplier ready   = sensor::isReady;

Supplier<Boolean> fonctionne, mais le BooleanSupplier primitif est ce que le JDK utilise pour les portes à court-circuit (Stream.iterate, IntStream.iterate dans leur forme à trois arguments prennent un BooleanSupplier ou IntPredicate comme test hasNext).

Supplier versus un argument T simple

La règle générale :

  • Passez une valeur quand le coût de son calcul est négligeable ou quand vous en avez définitivement besoin.
  • Passez un Supplier<T> quand le coût est significatif et que l'appelé pourrait ne pas avoir besoin de la valeur.
opt.orElse(loadDefaultFromDb());          // bad: loadDefaultFromDb() runs whether opt is present or not
opt.orElseGet(() -> loadDefaultFromDb()); // good: loadDefaultFromDb() runs only when opt is empty

Cette différence est la raison la plus fréquente pour laquelle orElseGet est préféré à orElse dans le code de production.

Un exemple pratique : Consumer.andThen, paresse du Supplier, variantes primitives

Le programme ci-dessous construit deux consumers et les enchaîne avec andThen, démontre la différence d'évaluation entre orElse et orElseGet avec un compteur, génère un petit flux depuis un Supplier, et associe IntConsumer à IntStream.forEach pour éviter tout autoboxing.

java— editable, runs on the server

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

  • log.andThen(store) a exécuté les deux consumers sur la même entrée, dans l'ordre de déclaration. Le journal d'audit a enregistré les deux appels ; la chaîne est devenue un seul Consumer<String> que l'on peut passer à forEach comme n'importe quel autre.
  • La chaîne andThen qui commence par boom s'est arrêtée à l'exception — never n'a jamais été invoqué. andThen est séquentiel, il n'avale pas les exceptions.
  • present.orElseGet(expensive) n'a pas touché le supplier parce que l'optional était présent, alors que present.orElse(expensive.get()) a évalué l'appel coûteux avant même d'en avoir besoin. Le compteur d'appels en est la preuve — c'est l'écart que Supplier existe pour combler.
  • Stream.generate(ids).limit(3) a produit trois UUID en appelant get() exactement trois fois. Le supplier est la source paresseuse d'un flux non borné — limit est ce qui rend le pipeline fini.
  • IntConsumer add s'est branché directement sur IntStream.forEach et a évité le boxing de chaque entier de la plage. Utilisez la spécialisation primitive dès que vous êtes dans un flux primitif.
  • BooleanSupplier underFive a illustré la forme que le JDK utilise pour la forme à trois arguments de Stream.iterate et autres portes « continuer jusqu'à » — le supplier est vérifié à chaque itération, de façon paresseuse.

Et ensuite

Vous avez maintenant vu les quatre coins : Function (entrée, sortie), Predicate (entrée, boolean), Consumer (entrée, pas de sortie), Supplier (pas d'entrée, sortie). Le prochain chapitre, Java BinaryOperator et UnaryOperator, clôt la partie avec les deux spécialisations où chaque paramètre partage le même type — la forme qui alimente Stream.reduce, Map.merge et List.replaceAll.

Entraînement

Pratique
Vous écrivez `String name = userOpt.orElseXxx(...)` et la valeur par défaut est `loadDefaultName()`, qui prend plusieurs secondes car elle accède à une base de données. Vous souhaitez que ce chargement ne s'exécute *que* si `userOpt` est vide. Quel appel est correct ?
Vous écrivez `String name = userOpt.orElseXxx(...)` et la valeur par défaut est `loadDefaultName()`, qui prend plusieurs secondes car elle accède à une base de données. Vous souhaitez que ce chargement ne s'exécute *que* si `userOpt` est vide. Quel appel est correct ?
Was this page helpful?