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, чтобы избежать автобоксинга.
Что следует из этого примера:
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.