W3docs

Java Consumer und Supplier

Consumer und Supplier als funktionale Interfaces in Java – Seiteneffekte und verzögerte Werterzeugung im Überblick.

Consumer<T> und Supplier<T> sind die beiden funktionalen Interfaces für die nicht-reinen Ecken der Vier-Ecken-Taxonomie:

  • Consumer<T> nimmt einen Wert entgegen und gibt nichts zurück — seine Aufgabe ist der Seiteneffekt (ausgeben, protokollieren, schreiben, in eine Sammlung einfügen).
  • Supplier<T> nimmt nichts entgegen und gibt einen Wert zurück — seine Aufgabe ist es, ein T faul, auf Anfrage zu erzeugen (Standardwerte, Factories, Zufallswerte).

Beide ergänzen die Function/Predicate-Kapitel, die zuvor behandelt wurden: Jene lieferten aus einem Wert einen anderen Wert, diese treten in die Außenwelt ein bzw. aus ihr heraus. Dieses Kapitel behandelt beide Interfaces gemeinsam, da ihre APIs sehr überschaubar sind und sich ihre Einsatzstellen überschneiden.

Consumer<T>

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

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

Ein Consumer ist „tu etwas mit diesem T." Die SAM-Methode ist accept. Die einzige Default-Methode andThen verkettet Consumer, sodass sie der Reihe nach auf dieselbe Eingabe angewendet werden:

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 bricht nicht ab, wenn der erste Consumer eine Ausnahme wirft — die Ausnahme wird weitergegeben, und der zweite Consumer wird nie ausgeführt. Das entspricht der gleichen Semantik wie zwei Aufrufe hintereinander in einem Block ohne try: Der Fehler stoppt die Sequenz.

Wo Consumer<T> vorkommt

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

Überall dort, wo das JDK sagt „tu etwas mit jedem Element", ist der Parameter ein Consumer<T> oder für zwei Argumente ein BiConsumer<K, V> (am bekanntesten: Map.forEach((k, v) -> ...)).

BiConsumer<T, U>

Die zweistellige Variante:

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 hat dieselbe andThen-Default-Methode. Es gibt kein BiSupplier — ein zweistelliger Supplier wäre schlicht eine BiFunction<T, U, R>.

Primitive Spezialisierungen — IntConsumer, LongConsumer, DoubleConsumer

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

Gleiche andThen-Semantik. IntStream.forEach akzeptiert einen IntConsumer, weshalb ein primitiver Stream Ihren Lambda-Ausdruck ohne Boxing aufrufen kann.

Außerdem gibt es ObjIntConsumer<T>, ObjLongConsumer<T> und ObjDoubleConsumer<T> für den Fall, dass ein Argument ein Objekt und das andere ein primitiver Typ ist — Stream.collect(Supplier, BiConsumer, BiConsumer) und seine primitiven Entsprechungen nutzen diese.

Supplier<T>

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

Das ist das gesamte Interface — keine Default-Methoden, kein andThen, keine Komposition. Der Grund: Ein Supplier ist die denkbar einfachste Form: null Eingaben, eine Ausgabe, und das Einzige, was man damit tun kann, ist get() aufzurufen.

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

Wo Supplier<T> vorkommt

Supplier ist die JDK-Art, Faulheit auszudrücken — „gib mir diesen Wert, aber erst wenn ich ihn brauche":

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

Jedes Mal, wenn ein Supplier<T> im JDK erscheint, lautet der Vertrag: „Dieser Wert wird möglicherweise nie benötigt." Optional.orElseGet ruft get() nur auf, wenn der Optional leer ist; Stream.generate ruft ihn nur auf, wenn das nächste Element angefordert wird. Diese Faulheit ist der eigentliche Zweck — ein einfaches T-Argument wäre bereits berechnet worden, bevor die Methode aufgerufen wurde.

Primitive Spezialisierungen — IntSupplier, LongSupplier, DoubleSupplier, BooleanSupplier

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

Supplier<Boolean> funktioniert, aber der primitive BooleanSupplier wird vom JDK für Kurzschluss-Bedingungen verwendet (Stream.iterate, IntStream.iterate in ihrer dreistelligen Form verwenden einen BooleanSupplier oder IntPredicate als hasNext-Test).

Supplier versus ein einfaches T-Argument

Die Faustregel:

  • Übergeben Sie einen Wert, wenn die Berechnungskosten vernachlässigbar sind oder wenn Sie den Wert auf jeden Fall benötigen.
  • Übergeben Sie einen Supplier<T>, wenn die Kosten relevant sind und der Aufgerufene den Wert möglicherweise nicht benötigt.
opt.orElse(loadDefaultFromDb());          // bad: loadDefaultFromDb() runs whether opt is present or not
opt.orElseGet(() -> loadDefaultFromDb()); // good: loadDefaultFromDb() runs only when opt is empty

Dieser Unterschied ist der häufigste Grund, warum orElseGet in Produktivcode gegenüber orElse bevorzugt wird.

Ein ausführliches Beispiel: Consumer.andThen, Supplier-Faulheit, primitive Varianten

Das folgende Programm erstellt zwei Consumer und verkettet sie mit andThen, demonstriert den Unterschied in der Auswertung zwischen orElse und orElseGet anhand eines Zählers, erzeugt einen kleinen Stream aus einem Supplier und kombiniert IntConsumer mit IntStream.forEach, sodass kein Autoboxing stattfindet.

java— editable, runs on the server

Was aus dem Programmlauf zu lernen ist:

  • log.andThen(store) hat beide Consumer auf dieselbe Eingabe in der Deklarationsreihenfolge angewendet. Der Audit-Trail zeigte beide Aufrufe; die Kette wurde zu einem einzigen Consumer<String>, der wie jeder andere an forEach übergeben werden kann.
  • Die andThen-Kette, die mit boom begann, stoppte bei der Ausnahme — never wurde nie aufgerufen. andThen ist sequenziell und schluckt keine Ausnahmen.
  • present.orElseGet(expensive) ließ den Supplier unberührt, weil der Optional befüllt war, während present.orElse(expensive.get()) den teuren Aufruf auswertete, bevor er überhaupt benötigt wurde. Der Aufrufzähler ist der Beweis — das ist die Lücke, die Supplier schließt.
  • Stream.generate(ids).limit(3) erzeugte drei UUIDs, indem get() genau dreimal aufgerufen wurde. Der Supplier ist die faule Quelle eines unbegrenzten Streams — limit macht die Pipeline endlich.
  • IntConsumer add passte direkt zu IntStream.forEach und vermied das Boxing jedes Integer-Werts im Bereich. Verwenden Sie die primitive Spezialisierung immer dann, wenn Sie sich innerhalb eines primitiven Streams befinden.
  • BooleanSupplier underFive zeigte die Form, die das JDK für die dreistellige Stream.iterate-Variante und andere „weitermachen bis"-Bedingungen verwendet — der Supplier wird einmal pro Iteration faul abgefragt.

Was kommt als Nächstes

Sie haben nun alle vier Ecken kennengelernt: Function (Eingabe, Ausgabe), Predicate (Eingabe, boolean), Consumer (Eingabe, keine Ausgabe), Supplier (keine Eingabe, Ausgabe). Das nächste Kapitel, Java BinaryOperator und UnaryOperator, schließt den Teil mit den beiden Spezialisierungen ab, bei denen alle Parameter denselben Typ haben — der Form, die Stream.reduce, Map.merge und List.replaceAll antreibt.

Übungen

Übung
Sie schreiben `String name = userOpt.orElseXxx(...)` und der Standardwert ist `loadDefaultName()`, was mehrere Sekunden dauert, weil eine Datenbank abgefragt wird. Sie möchten, dass dieser Aufruf *nur* dann stattfindet, wenn `userOpt` leer ist. Welcher Aufruf ist korrekt?
Sie schreiben `String name = userOpt.orElseXxx(...)` und der Standardwert ist `loadDefaultName()`, was mehrere Sekunden dauert, weil eine Datenbank abgefragt wird. Sie möchten, dass dieser Aufruf *nur* dann stattfindet, wenn `userOpt` leer ist. Welcher Aufruf ist korrekt?
Was this page helpful?