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 unTde 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.
Où 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 samePartout 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();Où 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 threadChaque 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 emptyCette 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.
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 seulConsumer<String>que l'on peut passer àforEachcomme n'importe quel autre.- La chaîne
andThenqui commence parbooms'est arrêtée à l'exception —nevern'a jamais été invoqué.andThenest 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 quepresent.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 queSupplierexiste pour combler.Stream.generate(ids).limit(3)a produit trois UUID en appelantget()exactement trois fois. Le supplier est la source paresseuse d'un flux non borné —limitest ce qui rend le pipeline fini.IntConsumer adds'est branché directement surIntStream.forEachet 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 underFivea illustré la forme que le JDK utilise pour la forme à trois arguments deStream.iterateet 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.