Интерфейс Consumer в Java
Две строки, которые ведут себя не так, как ожидает большинство:
Consumer<String> printUpper = s -> System.out.println(s.toUpperCase());
printUpper.andThen(s -> System.out.println(s.length()));
printUpper.accept("hello"); // HELLO — и всё, длина не напечаталась Метод andThen() ничего не меняет в исходном Consumer — он собирает и возвращает новый. Если результат не сохранить и не вызвать accept() именно на нём, второе действие просто теряется. Разберём интерфейс целиком, чтобы такие вещи были очевидны.
Что такое Consumer
Consumer — встроенный функциональный интерфейс из пакета java.util.function, добавленный в Java SE 8. Он принимает один аргумент и не возвращает результат: его задача — побочный эффект (вывод, запись в лог, изменение состояния объекта, отправка сообщения).
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
} Функциональный дескриптор интерфейса:
T -> void Читается так: «на вход — объект типа T, на выход — ничего». Именно поэтому Consumer — единственный из базовых функциональных интерфейсов, который имеет смысл только благодаря побочным эффектам: если убрать из тела лямбды всё, что меняет внешний мир, вызов станет бессмысленным.
Метод accept()
accept(T t) — единственный абстрактный метод интерфейса. Он и выполняет действие:
import java.util.function.Consumer;
public class ConsumerExample1 {
public static void main(String[] args) {
Consumer<String> printUpperCase = str -> System.out.println(str.toUpperCase());
printUpperCase.accept("hello"); // HELLO
}
} Лямбду часто можно заменить ссылкой на метод — код становится короче и читается лучше:
Consumer<String> print = System.out::println;
print.accept("hello"); // hello
Consumer<List<String>> clear = List::clear; // ссылка на метод экземпляра Полезно знать
Ссылка на метод System.out::println вычисляет System.out один раз — в момент создания Consumer, а не при каждом вызове accept(). Если после этого подменить поток вывода через System.setOut(), уже созданный Consumer продолжит писать в старый поток. Лямбда s -> System.out.println(s) ведёт себя иначе: она читает System.out при каждом вызове.
Композиция через andThen()
Consumer содержит один метод по умолчанию. Он возвращает составной Consumer, который последовательно выполняет два действия над одним и тем же аргументом:
default Consumer<T> andThen(Consumer<? super T> after) Реализация в JDK предельно простая — она и объясняет всё поведение метода:
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
} Обратите внимание: accept() ничего не возвращает, а вот andThen() возвращает новый объект Consumer. Его нужно сохранить или сразу использовать:
import java.util.function.Consumer;
public class ConsumerExample2 {
public static void main(String[] args) {
Consumer<String> printUpperCase = str -> System.out.println(str.toUpperCase());
Consumer<String> printLowerCase = str -> System.out.println(str.toLowerCase());
Consumer<String> printBoth = printUpperCase.andThen(printLowerCase);
printBoth.accept("Hello World");
// HELLO WORLD
// hello world
}
} Вызовы andThen() можно выстраивать в цепочку любой длины — порядок выполнения совпадает с порядком записи слева направо:
Consumer<String> pipeline = printUpperCase
.andThen(printLowerCase)
.andThen(s -> System.out.println("длина: " + s.length()));
pipeline.accept("Java");
// JAVA
// java
// длина: 4 Где Consumer применяется в JDK
Часто пишут, что главное применение Consumer — вывод на консоль. Это лишь самый наглядный пример. На практике интерфейс чаще встречается не в вашем коде напрямую, а в сигнатурах методов стандартной библиотеки:
Iterable.forEach(Consumer<? super T>)— обход любой коллекции;Stream.forEach(Consumer<? super T>)— терминальная операция стрима;Stream.peek(Consumer<? super T>)— промежуточная операция для отладки конвейера;Optional.ifPresent(Consumer<? super T>)— действие, если значение есть;Map.forEach(BiConsumer<? super K, ? super V>)— обход пар «ключ — значение».
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
public class ConsumerExample3 {
public static void main(String[] args) {
Consumer<String> greet = name -> System.out.println("Привет, " + name + "!");
List<String> names = List.of("Anna", "Bob");
names.forEach(greet);
// Привет, Anna!
// Привет, Bob!
Optional.of("Clara").ifPresent(greet); // Привет, Clara!
Map<String, Integer> ages = Map.of("Anna", 30);
ages.forEach((k, v) -> System.out.println(k + " -> " + v)); // Anna -> 30
}
} Семейство Consumer: BiConsumer, IntConsumer и другие
В пакете java.util.function у Consumer есть несколько родственных интерфейсов: для двух аргументов и для примитивов (последние избавляют от автоупаковки и лишних объектов).
| Интерфейс | Абстрактный метод | Дескриптор | Когда нужен |
|---|---|---|---|
Consumer<T> | void accept(T t) | T -> void | Базовый случай: одно действие над одним объектом |
BiConsumer<T, U> | void accept(T t, U u) | (T, U) -> void | Два аргумента, например Map.forEach() |
IntConsumer | void accept(int value) | int -> void | IntStream.forEach() без упаковки в Integer |
LongConsumer | void accept(long value) | long -> void | То же самое для long |
DoubleConsumer | void accept(double value) | double -> void | То же самое для double |
ObjIntConsumer<T> | void accept(T t, int value) | (T, int) -> void | Аккумуляторы в IntStream.collect() |
У BiConsumer, IntConsumer, LongConsumer и DoubleConsumer тоже есть andThen() — с параметром того же типа. У ObjIntConsumer и его аналогов метода andThen() нет.
import java.util.function.IntConsumer;
import java.util.stream.IntStream;
IntConsumer printSquare = n -> System.out.println(n * n);
IntStream.rangeClosed(1, 3).forEach(printSquare);
// 1
// 4
// 9 На чём чаще всего ошибаются
1. Результат andThen() не сохранили
Тот самый случай из начала урока. Consumer неизменяем: andThen() создаёт новый объект, а старая переменная остаётся прежней.
printUpper.andThen(printLower); // результат выброшен, ничего не изменилось
printUpper.accept("hi"); // HI
Consumer<String> both = printUpper.andThen(printLower); // так правильно
both.accept("hi"); // HI, затем hi 2. Исключение в первом действии отменяет второе
Составной Consumer не перехватывает исключения. Если первое действие бросило исключение, второе не выполнится, а исключение уйдёт вызывающему коду.
Consumer<String> broken = s -> { throw new IllegalStateException("boom"); };
Consumer<String> print = System.out::println;
broken.andThen(print).accept("hi");
// IllegalStateException: boom — строка "hi" так и не напечатана 3. Попытка изменить элемент коллекции присваиванием
Присваивание параметру лямбды компилируется, но меняет только локальную копию ссылки — коллекция остаётся прежней.
List<String> names = new ArrayList<>(List.of("anna", "bob"));
names.forEach(s -> s = s.toUpperCase()); // бесполезно
System.out.println(names); // [anna, bob]
names.replaceAll(String::toUpperCase); // так работает
System.out.println(names); // [ANNA, BOB] 4. Проверяемое исключение внутри лямбды
Метод accept() не объявляет throws, поэтому проверяемое исключение обязано быть обработано прямо в теле лямбды — иначе код не скомпилируется.
Consumer<Path> reader = path -> Files.readAllLines(path); // ошибка компиляции: IOException
Consumer<Path> safeReader = path -> { // так компилируется
try {
Files.readAllLines(path).forEach(System.out::println);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}; 5. andThen(null)
Из-за Objects.requireNonNull(after) исключение NullPointerException прилетит сразу при вызове andThen(), а не потом, при accept(). Это удобно: ошибка обнаруживается в месте сборки цепочки.
Важно
Если действие должно вернуть значение — вам нужен не Consumer, а Function (и map() вместо forEach()). Признак неудачного выбора: внутри Consumer появляется запись во внешний список или в поле, чтобы «вынести» результат наружу. В параллельном стриме такой код к тому же перестаёт быть потокобезопасным.
Часто задаваемые вопросы
Чем Consumer отличается от Supplier, Function и Runnable?
Различие только в наличии входа и выхода. Consumer принимает аргумент и ничего не возвращает (T → void). Supplier наоборот: аргументов нет, значение возвращается (void → T). Function принимает аргумент и возвращает результат (T → R). Runnable не принимает и не возвращает ничего (void → void). Все четыре интерфейса не хранят состояния и описывают ровно одно действие.
В каком порядке выполняются действия в andThen и что будет при исключении?
Строго слева направо: сначала accept() у исходного Consumer, затем accept() у переданного в andThen(). Оба получают один и тот же аргумент. Исключения не перехватываются: если первое действие бросило исключение, второе не выполнится, а исключение уйдёт наверх из вызова accept() составного Consumer.
Почему в andThen параметр объявлен как Consumer<? super T>, а не Consumer<T>?
Это правило PECS: producer extends, consumer super. Объект типа T только передаётся внутрь, поэтому подойдёт любой обработчик, умеющий принимать T или его супертип. Благодаря этому к Consumer<String> можно присоединить Consumer<Object> — например, универсальный логгер. С сигнатурой Consumer<T> такая комбинация не скомпилировалась бы.
Можно ли изменить объект внутри Consumer?
Изменить состояние переданного объекта можно: вызов вроде user.setName("Anna") внутри лямбды сработает, потому что меняется сам объект. А вот присвоить параметру лямбды новую ссылку бесполезно — изменится только локальная переменная, вызывающий код этого не увидит. Для замены элементов коллекции используйте replaceAll() или map() в стриме.
Видео объяснение
Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.
Комментарии