W3docs

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 un T in 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 same

Ovunque 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 thread

Ogni 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 empty

Questa 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.

java— editable, runs on the server

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 singolo Consumer<String> passabile a forEach come qualsiasi altro.
  • La catena andThen che iniziava con boom si è interrotta all'eccezione — never non è stato mai invocato. andThen è sequenziale, non sopprime le eccezioni.
  • present.orElseGet(expensive) ha lasciato il supplier intatto perché l'optional era presente, mentre present.orElse(expensive.get()) ha valutato la chiamata costosa prima ancora che fosse necessaria. Il contatore delle chiamate è la prova — è il divario che Supplier esiste per colmare.
  • Stream.generate(ids).limit(3) ha prodotto tre UUID chiamando get() esattamente tre volte. Il supplier è la sorgente lazy di uno stream illimitato — limit è ciò che rende la pipeline finita.
  • IntConsumer add si è collegato direttamente a IntStream.forEach evitando il boxing di ogni intero nell'intervallo. Usa la specializzazione primitiva ogni volta che sei all'interno di uno stream primitivo.
  • BooleanSupplier underFive ha mostrato la forma che il JDK usa per la forma a tre argomenti di Stream.iterate e 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.

Esercizio

Pratica
Stai scrivendo `String name = userOpt.orElseXxx(...)` e il valore predefinito è `loadDefaultName()`, che richiede diversi secondi perché accede a un database. Vuoi che il caricamento avvenga *solo* se `userOpt` è vuoto. Quale chiamata è corretta?
Stai scrivendo `String name = userOpt.orElseXxx(...)` e il valore predefinito è `loadDefaultName()`, che richiede diversi secondi perché accede a un database. Vuoi che il caricamento avvenga *solo* se `userOpt` è vuoto. Quale chiamata è corretta?
Was this page helpful?