Java Consumer e Supplier
Consumer con effetti collaterali e Supplier produttori di valori: le interfacce funzionali Java spiegate con esempi pratici.
Consumer<T> e Supplier<T> sono le due interfacce funzionali per gli angoli non puri della tassonomia a quattro quadranti:
Consumer<T>riceve un valore e non restituisce nulla — il suo compito è l'effetto collaterale (stampare, registrare, scrivere, aggiungere a una collezione).Supplier<T>non riceve nulla e restituisce un valore — il suo compito è produrre unTin modo lazy, su richiesta (valori predefiniti, factory, casualità).
Entrambe si affiancano ai capitoli su Function/Predicate trattati in precedenza: quelle restituivano un valore da un valore, queste entrano e escono dal mondo circostante. Questo capitolo copre entrambe le interfacce perché le loro API sono ridotte e i loro utilizzi si sovrappongono.
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 significa "fai qualcosa con questo T." Il SAM è accept. L'unico metodo di default andThen concatena i consumer in modo che vengano eseguiti in sequenza sullo stesso input:
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 non fa short-circuit se il primo consumer lancia un'eccezione — lascia che l'eccezione si propaghi e il secondo consumer non viene mai eseguito. È la stessa semantica di scrivere le due chiamate in un blocco senza try: il fallimento interrompe la sequenza.
Dove appare Consumer<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 sameOvunque il JDK dica "fai qualcosa con ogni elemento," il parametro è un Consumer<T> oppure un BiConsumer<K, V> per i casi a due argomenti (in particolare Map.forEach((k, v) -> ...)).
BiConsumer<T, U>
La variante a due argomenti:
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 ha lo stesso metodo di default andThen. Non esiste un BiSupplier — un Supplier a due argomenti sarebbe semplicemente una BiFunction<T, U, R>.
Specializzazioni primitive — IntConsumer, LongConsumer, DoubleConsumer
IntConsumer printInt = System.out::println; // accepts int, no boxing
LongConsumer tally = n -> total += n;
DoubleConsumer record = d -> samples.add(d);Stessa semantica di andThen. IntStream.forEach accetta un IntConsumer, motivo per cui uno stream primitivo può invocare la lambda senza boxing.
Esistono anche ObjIntConsumer<T>, ObjLongConsumer<T>, ObjDoubleConsumer<T> per il caso in cui un argomento è un oggetto e l'altro è un primitivo — Stream.collect(Supplier, BiConsumer, BiConsumer) e i suoi cugini primitivi li utilizzano.
Supplier<T>
@FunctionalInterface
public interface Supplier<T> {
T get(); // the only abstract method
}Questa è l'intera interfaccia — nessun metodo di default, nessun andThen, nessuna composizione. Il motivo è che un Supplier è la forma più semplice possibile: zero input, un output, e l'unica cosa che puoi fare è chiamare get().
Supplier<List<String>> empty = ArrayList::new;
Supplier<UUID> id = UUID::randomUUID;
Supplier<String> expensive = () -> loadFromDb();Dove appare Supplier<T>
Supplier è il modo del JDK per scrivere lazy — "dammi questo valore, ma solo quando ne ho bisogno":
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 threadOgni volta che un Supplier<T> compare nel JDK, il contratto è "questo valore potrebbe non essere mai necessario." Optional.orElseGet chiama get() solo quando l'optional è vuoto; Stream.generate lo chiama solo quando viene richiesto l'elemento successivo. Quella laziness è il punto centrale — un argomento T semplice sarebbe già stato calcolato nel momento in cui il metodo viene invocato.
Specializzazioni primitive — IntSupplier, LongSupplier, DoubleSupplier, BooleanSupplier
IntSupplier count = () -> counter.getAndIncrement();
DoubleSupplier random = Math::random;
BooleanSupplier ready = sensor::isReady;Supplier<Boolean> funziona, ma il BooleanSupplier primitivo è ciò che il JDK usa per i gate di cortocircuito (Stream.iterate, IntStream.iterate nella forma a tre argomenti accettano un BooleanSupplier o IntPredicate come test hasNext).
Supplier versus un argomento T semplice
La regola empirica:
- Passa un valore quando il costo di calcolo è trascurabile oppure quando ne hai sicuramente bisogno.
- Passa un
Supplier<T>quando il costo è rilevante e il chiamato potrebbe non aver bisogno del valore.
opt.orElse(loadDefaultFromDb()); // bad: loadDefaultFromDb() runs whether opt is present or not
opt.orElseGet(() -> loadDefaultFromDb()); // good: loadDefaultFromDb() runs only when opt is emptyQuesta differenza è il motivo più comune per cui orElseGet viene preferito a orElse nel codice in produzione.
Un esempio pratico: Consumer.andThen, laziness di Supplier, varianti primitive
Il programma seguente costruisce due consumer e li concatena con andThen, dimostra la differenza di valutazione tra orElse e orElseGet con un contatore, genera un piccolo stream da un Supplier e abbina IntConsumer con IntStream.forEach evitando autoboxing.
Cosa trarre dall'esecuzione:
log.andThen(store)ha eseguito entrambi i consumer sullo stesso input, nell'ordine di dichiarazione. L'audit trail ha mostrato entrambe le chiamate; la catena è diventata un singoloConsumer<String>passabile aforEachcome qualsiasi altro.- La catena
andThenche iniziava conboomsi è interrotta all'eccezione —nevernon è stato mai invocato.andThenè sequenziale, non sopprime le eccezioni. present.orElseGet(expensive)ha lasciato il supplier intatto perché l'optional era presente, mentrepresent.orElse(expensive.get())ha valutato la chiamata costosa prima ancora che fosse necessaria. Il contatore delle chiamate è la prova — è il divario cheSupplieresiste per colmare.Stream.generate(ids).limit(3)ha prodotto tre UUID chiamandoget()esattamente tre volte. Il supplier è la sorgente lazy di uno stream illimitato —limitè ciò che rende la pipeline finita.IntConsumer addsi è collegato direttamente aIntStream.forEachevitando il boxing di ogni intero nell'intervallo. Usa la specializzazione primitiva ogni volta che sei all'interno di uno stream primitivo.BooleanSupplier underFiveha mostrato la forma che il JDK usa per la forma a tre argomenti diStream.iteratee altri gate "continua finché" — il supplier viene controllato una volta per iterazione, in modo lazy.
Cosa viene dopo
Hai ora visto tutti e quattro i quadranti: Function (in, out), Predicate (in, boolean), Consumer (in, nessun out), Supplier (nessun in, out). Il prossimo capitolo, Java BinaryOperator e UnaryOperator, chiude la parte con le due specializzazioni in cui ogni parametro condivide lo stesso tipo — la forma che alimenta Stream.reduce, Map.merge e List.replaceAll.