Lambda ·
‹ Предыдущий Следующий ›
⏱ 5 минут чтения Обновлено: 2026-09-07

Интерфейс 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, а Functionmap() вместо 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() в стриме.

Видео объяснение

Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.

Комментарии

Зарегистрируйтесь или войдите, чтобы иметь возможность оставить комментарий.