W3docs

Java Consumer и Supplier

Consumer с побочными эффектами и Supplier, возвращающий значения — функциональные интерфейсы Java с примерами.

Consumer<T> и Supplier<T> — два функциональных интерфейса для нечистых углов четырёхугольной таксономии:

  • Consumer<T> принимает значение и возвращает ничего — его задача — побочный эффект (вывод, логирование, запись, добавление в коллекцию).
  • Supplier<T> принимает ничего и возвращает значение — его задача — лениво, по требованию производить T (значения по умолчанию, фабрики, случайные числа).

Оба дополняют главы о Function/Predicate, рассмотренные ранее: те возвращали значение из значения, эти взаимодействуют с внешним миром. В данной главе рассматриваются оба интерфейса, поскольку их API невелики, а области применения перекрываются.

Consumer<T>

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

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

Consumer — это «сделай что-нибудь с этим T». SAM-метод — accept. Единственный дефолтный метод andThen объединяет потребителей в цепочку, так что они выполняются последовательно на одном входе:

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 не прерывается, если первый потребитель бросает исключение — оно распространяется дальше, и второй потребитель не выполняется. Семантика та же, что при записи двух вызовов подряд в блоке без try: ошибка останавливает цепочку.

Где используется 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

Везде, где JDK говорит «сделай что-то с каждым элементом», параметр — это Consumer<T> или BiConsumer<K, V> для двухаргументных случаев (в первую очередь Map.forEach((k, v) -> ...)).

BiConsumer<T, U>

Двухаргументный вариант:

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 есть тот же дефолтный метод andThen. BiSupplier не существует — двухаргументный Supplier был бы просто BiFunction<T, U, R>.

Примитивные специализации — IntConsumer, LongConsumer, DoubleConsumer

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

Та же семантика andThen. IntStream.forEach принимает IntConsumer, поэтому примитивный поток может вызвать вашу лямбду без боксинга.

Существуют также ObjIntConsumer<T>, ObjLongConsumer<T>, ObjDoubleConsumer<T> для случаев, когда один аргумент — объект, а другой — примитив. Их используют Stream.collect(Supplier, BiConsumer, BiConsumer) и его примитивные варианты.

Supplier<T>

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

Это весь интерфейс — без дефолтных методов, без andThen, без композиции. Причина в том, что Supplier — простейшая возможная форма: ноль входов, один выход, и единственное, что с ним можно сделать, — вызвать get().

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

Где используется Supplier<T>

Supplier — это способ JDK выразить ленивость — «дай мне это значение, но только когда оно понадобится»:

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

В каждом месте, где Supplier<T> встречается в JDK, контракт таков: «это значение может никогда не понадобиться». Optional.orElseGet вызывает get() только когда optional пуст; Stream.generate вызывает его только при запросе следующего элемента. Именно в этой ленивости и состоит смысл — обычный аргумент T уже был бы вычислен к моменту вызова метода.

Примитивные специализации — IntSupplier, LongSupplier, DoubleSupplier, BooleanSupplier

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

Supplier<Boolean> работает, но примитивный BooleanSupplier — это то, что JDK использует для ворот с коротким замыканием (Stream.iterate, IntStream.iterate в трёхаргументной форме принимают BooleanSupplier или IntPredicate в качестве теста hasNext).

Supplier против обычного аргумента T

Практическое правило:

  • Передавайте значение, когда стоимость его вычисления пренебрежимо мала или когда оно точно потребуется.
  • Передавайте Supplier<T>, когда стоимость важна и вызываемый код может не нуждаться в значении.
opt.orElse(loadDefaultFromDb());          // bad: loadDefaultFromDb() runs whether opt is present or not
opt.orElseGet(() -> loadDefaultFromDb()); // good: loadDefaultFromDb() runs only when opt is empty

Это различие — самая распространённая причина, по которой в production-коде предпочитают orElseGet над orElse.

Практический пример: Consumer.andThen, ленивость Supplier, примитивные варианты

Программа ниже строит двух потребителей и объединяет их через andThen, демонстрирует разницу в вычислении между orElse и orElseGet с помощью счётчика, генерирует небольшой поток из Supplier и сопрягает IntConsumer с IntStream.forEach, чтобы избежать автобоксинга.

java— editable, runs on the server

Что следует из этого примера:

  • log.andThen(store) запустил оба потребителя на одном входе, в порядке объявления. Журнал аудита отразил оба вызова; цепочка стала единым Consumer<String>, который можно передать в forEach как любой другой.
  • Цепочка andThen, начавшаяся с boom, остановилась на исключении — never так и не был вызван. andThen последователен, но не поглощает исключения.
  • present.orElseGet(expensive) не тронул supplier, поскольку optional содержал значение, тогда как present.orElse(expensive.get()) вычислил дорогостоящий вызов ещё до того, как он понадобился. Счётчик вызовов — тому доказательство: именно этот разрыв призван устранить Supplier.
  • Stream.generate(ids).limit(3) создал три UUID, вызвав get() ровно три раза. Supplier — ленивый источник неограниченного потока, а limit делает конвейер конечным.
  • IntConsumer add напрямую подключился к IntStream.forEach и избежал боксинга каждого целого числа в диапазоне. Используйте примитивную специализацию везде, где работаете с примитивным потоком.
  • BooleanSupplier underFive показал форму, которую JDK использует для трёхаргументного Stream.iterate и других ворот «продолжать до тех пор» — supplier проверяется на каждой итерации, лениво.

Что дальше

Теперь вы видели все четыре угла: Function (вход, выход), Predicate (вход, boolean), Consumer (вход, без выхода), Supplier (без входа, выход). Следующая глава, Java BinaryOperator и UnaryOperator, завершает раздел двумя специализациями, где все параметры имеют одинаковый тип — форма, лежащая в основе Stream.reduce, Map.merge и List.replaceAll.

Практика

Практика
Вы пишете `String name = userOpt.orElseXxx(...)`, а значение по умолчанию — `loadDefaultName()`, которая занимает несколько секунд из-за обращения к базе данных. Вы хотите, чтобы загрузка выполнялась *только* если `userOpt` пуст. Какой вызов правильный?
Вы пишете `String name = userOpt.orElseXxx(...)`, а значение по умолчанию — `loadDefaultName()`, которая занимает несколько секунд из-за обращения к базе данных. Вы хотите, чтобы загрузка выполнялась *только* если `userOpt` пуст. Какой вызов правильный?
Was this page helpful?