В этой статье мы поговорим о функциональном интерфейсе Consumer, доступном в Java начиная с версии 1.8. Это функциональный интерфейс, который представляет собой операцию, принимающую единственный аргумент и не возвращающую никакого результата.
Интерфейс полезен, когда необходимо итерироваться по коллекции или выполнить некоторое callback-действие на стороне вызывающего кода.
Рассмотрим следующий пример класса ConsumerExample:
package ru.allineed.samples.consumer_interface;
import ru.allineed.samples.config.Localization;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
public class ConsumerExample {
static class Animal {
private String name;
private String color;
public Animal(String name, String color) {
this.name = name;
this.color = color;
}
@Override
public String toString() {
return "Animal{" +
"name='" + name + '\'' +
", color='" + color + '\'' +
'}';
}
}
public static void main(String[] args) {
List<Animal> animals = Arrays.asList(new Animal("Cat", "black"), new Animal("Dog", "brown"));
Consumer<Animal> animalConsumer = animal -> System.out.println("Feeding the animal: " + animal);
// [RU] Передаём экземпляр интерфейса Consumer в forEach:
// [EN] Passing an instance of Consumer interface into forEach:
Localization.printLocalized(
"Передаём экземпляр интерфейса Consumer в forEach:",
"Passing an instance of Consumer interface into forEach:");
animals.forEach(animalConsumer);
}
}
Сразу скажу, опережая вопросы, что данный сниппет используется как часть общего проекта Java-примеров, ссылку на Git-репозиторий которого и инструкцию по запуску всех примеров вы найдете внизу этой статьи. Вызов Localization.printLocalized() - это подготовленный нами утильный метод, внутри фактически представляющий собой System.out.println(), но с поддержкой русского и английского языка в примерах.
Внутри класса ConsumerExample задаётся простой статический класс Animal, который описывает некоторое абстрактное животное и имеет два строковых поля - name (хранит название животного) и color (хранит название цвета животного). Также для класса задан конструктор с двумя параметрами, который призван установить значения данных полей для создаваемого объекта.
В основном классе есть стандартная точка входа - статический метод main(), с которого начинается выполнение нашей программы-примера.
В начале мы создаём список animals, инициализируя его двумя объектами нашего класса Animal:
List<Animal> animals = Arrays.asList(new Animal("Cat", "black"), new Animal("Dog", "brown"));
Добавили в список два объекта - кошку и собаку, установив их цвет в чёрный ("black") и коричневый ("brown"), соответственно.
Далее мы как раз определяем инстанс интерфейса Consumer, передавая ему тип нашего класса Animal и задаём лямбда-выражение, определяющее ту самую операцию, которую нужно будет выполнить над переданным объектом класса. В данном случае мы просто выводим на экран строку "Feeding the animal" ("Кормим животное:") и далее выводим строковое представление нашего объекта животного, которое передаётся в Consumer:
Consumer<Animal> animalConsumer = animal -> System.out.println("Feeding the animal: " + animal);
Как можно видеть, в классе Animal переопределён метод toString(), который как раз выведет строковое представление объекта "животное" с указанием его названия и цвета.
Наконец, в третьей строке метода main() мы пробегаем по нашему списку с животными с помощью оператора forEach, которому на вход подаётся созданный нами экземпляр интерфейса Consumer:
animals.forEach(animalConsumer);
Что в итоге будет происходить при выполнении программы? Оператор forEach выполнит заданную нами операцию вывода на экран строки "Feeding the animal: " вместе со строковым представлением текущего объекта животное.
В консоли при запуске мы увидим следующий ожидаемый вывод:
Feeding the animal: Animal{name='Cat', color='black'}
Feeding the animal: Animal{name='Dog', color='yellow'}
Process finished with exit code 0
В этом примере мы явно создавали экземпляр интерфейса Consumer с указанием лямбда-выражения для нашей операции. Можно было бы сделать метод main() короче и выполнять операцию по выводу информации о наших животных "на лету", т.е. передавать лямбду сразу в оператор forEach:
public static void main(String[] args) {
List<Animal> animals = Arrays.asList(new Animal("Cat", "black"), new Animal("Dog", "brown"));
animals.forEach(animal -> System.out.println("Feeding the animal: " + animal));
}
Результат выполнения ничем не будет отличаться от предыдущего.
Третий вариант, как можно использовать экземпляр функционального интерфейса Consumer - создавать анонимный инстанс и передавать его в оператор forEach:
public static void main(String[] args) {
List<Animal> animals = Arrays.asList(new Animal("Cat", "black"), new Animal("Dog", "brown"));
animals.forEach(new Consumer<Animal>() {
@Override
public void accept(Animal animal) {
System.out.println("Feeding the animal: " + animal);
}
});
}
Этот вариант выглядит более громоздким, чем предыдущие, более того, если Вы пишете этот код в среде разработки IntelliJ IDEA, то она предложит Вам оптимизировать этот участок и заменить его лямбдой - при наведении на "серый" участок кода Вы увидите сообщение "Anonymous new Consumer<Animal>() can be replaced with lambda":
Давайте рассмотрим ещё один интересный пример, где нас может выручить интерфейс Consumer.
Предположим, у вас есть отдельный метод doSomeOperation(), и список объектов Вы задаёте именно в нём (и как-то их обрабатываете, к примеру). При этом в определённой точке Вам требуется перебрать все объекты в списке и выполнить для них какую-то операцию, которая сама задаётся извне, в коде, вызывающем Ваш отдельный метод.
Тогда для метода doSomeOperation() можно в качестве входного аргумента указать как раз Consumer для достижения этой цели. Вот как может выглядеть код:
public static void main(String[] args) {
Consumer<Animal> animalConsumer = animal -> System.out.println("Feeding the animal: " + animal);
doSomeOperation(animalConsumer);
}
public static void doSomeOperation(Consumer<Animal> animalConsumer) {
List<Animal> animals = Arrays.asList(new Animal("Cat", "black"), new Animal("Dog", "brown"));
Localization.printLocalized(
"Перед выполнением операции",
"Before the operation");
animals.forEach(animalConsumer);
Localization.printLocalized(
"После выполнения операции",
"After the operation");
}
Обратите внимание, сам Consumer мы задали до вызова метода doSomeOperation() и передаем созданный экземпляр консьюмера в него в качестве параметра. В самом методе задаётся список объектов (в нашем случае, тех же животных, для примера), и дальше выполняются какие-то операции до и после перебора элементов списка.
Результатом программы будет следующий вывод на консоль:
Перед выполнением операции
Feeding the animal: Animal{name='Cat', color='black'}
Feeding the animal: Animal{name='Dog', color='brown'}
После выполнения операции
Process finished with exit code 0
Данный подход полезен, когда Вам необходимо связать разные участки Вашей программы и разделить логику обработки подобным образом.
На этом всё, надеюсь, статья пригодится и поможет чаще использовать полезный функциональный интерфейс Consumer в Ваших программах. Буду благодарен за отзывы или вопросы в комментарии к статье. Удачи!
Примеры из этой статьи: https://github.com/AllineedRu/JavaExamples/blob/main/allineed-core/src/main/java/ru/allineed/samples/consumer_interface/ConsumerExample.java