JavaRush /Курсы /JAVA 25 SELF /Функциональные интерфейсы: Predicate, Consumer, Supplier,...

Функциональные интерфейсы: Predicate, Consumer, Supplier, Function

JAVA 25 SELF
49 уровень , 0 лекция
Открыта

1. Углубляемся в функциональный интерфейс

Функциональный интерфейс — это такой интерфейс, в котором есть РОВНО один абстрактный (то есть не реализованный) метод. Именно благодаря этому Java понимает: «ага, сюда можно подставить лямбду!» или ссылку на метод.

Чтобы никто не ошибся, в таких интерфейсах обычно стоит аннотация @FunctionalInterface. Она не обязательна, но если вы её добавили и случайно написали второй абстрактный метод, компилятор тут же начнёт возмущаться.

Пример:

@FunctionalInterface
interface MyAction {
    void run();
}
MyAction action = () -> System.out.println("Привет из лямбды!");
action.run(); // Выведет: Привет из лямбды!

Зачем это нужно?

  • Позволяет использовать лямбда-выражения и ссылки на методы вместо создания анонимных классов (меньше бойлерплейта!).
  • Дает компилятору понять, что интерфейс предназначен для функционального программирования.

Интересный факт: В стандартной библиотеке Java уже есть десятки таких интерфейсов — не нужно изобретать велосипед!

2. Обзор стандартных функциональных интерфейсов

В пакете java.util.function живут десятки функциональных интерфейсов. Рассмотрим четыре самых популярных (у них самая высокая «посещаемость» среди всех Java-интерфейсов).

Интерфейс Что принимает Что возвращает Для чего обычно используется
Predicate<T>
T
boolean
Проверка условия (фильтрация)
Consumer<T>
T
void
Выполнить действие над объектом
Supplier<T>
ничего
T
Получить/сгенерировать объект
Function<T, R>
T
R
Преобразовать T в R

Predicate<T>

Описание: Функция, которая принимает объект типа T и возвращает true или false. Типичный пример: фильтрация списка. Ключевой метод — test.

Predicate<String> isLong = s -> s.length() > 5;
System.out.println(isLong.test("Java"));       // false
System.out.println(isLong.test("Functional")); // true

Consumer<T>

Описание: Принимает объект типа T и делает с ним действие, ничего не возвращает. Ключевой метод — accept.

Consumer<String> printer = s -> System.out.println("Печатаю: " + s);
printer.accept("Hello, world!"); // Печатаю: Hello, world!

Supplier<T>

Описание: Ничего не принимает, возвращает объект типа T. Можно представить как «генератор» значений. Ключевой метод — get.

Supplier<Double> randomSupplier = () -> Math.random();
System.out.println(randomSupplier.get()); // Например, 0.1234567

Function<T, R>

Описание: Принимает объект типа T и возвращает объект типа R. Типичный пример: преобразование данных. Ключевой метод — apply.

Function<String, Integer> stringToLength = s -> s.length();
System.out.println(stringToLength.apply("Java")); // 4

Кратко: UnaryOperator, BinaryOperator, BiFunction

  • UnaryOperator<T> — то же, что Function<T, T>: принимает и возвращает один и тот же тип.
  • BinaryOperator<T> — то же, что BiFunction<T, T, T>: принимает два T, возвращает один T.
  • BiFunction<T, U, R> — принимает два разных типа, возвращает третий.
UnaryOperator<Integer> square = x -> x * x;
BinaryOperator<Integer> sum = (a, b) -> a + b;
BiFunction<String, Integer, String> repeat = (s, n) -> s.repeat(n);

3. Примеры использования

Посмотрим, как эти интерфейсы встречаются в реальных задачах и особенно — в коллекциях и Stream API.

Передача в методы коллекций и Stream API

Пример 1: Predicate и фильтрация

List<String> words = List.of("java", "stream", "lambda", "code");
List<String> longWords = words.stream()
    .filter(word -> word.length() > 4) // Predicate<String>
    .toList();
System.out.println(longWords); // [stream, lambda]

Пример 2: Consumer и forEach

words.forEach(word -> System.out.println("Слово: " + word)); // Consumer<String>

Пример 3: Function и map

List<Integer> lengths = words.stream()
    .map(word -> word.length()) // Function<String, Integer>
    .toList();
System.out.println(lengths); // [4, 6, 6, 4]

Пример 4: Supplier и генерация значений

Supplier<String> greetingSupplier = () -> "Привет, Java!";
System.out.println(greetingSupplier.get()); // Привет, Java!

Сравнение с анонимными классами

Раньше приходилось писать вот так:

Predicate<String> isShort = new Predicate<String>() {
    @Override
    public boolean test(String s) {
        return s.length() < 5;
    }
};

С лямбдами стало в разы приятнее:

Predicate<String> isShort = s -> s.length() < 5;

4. Практика: пишем лямбда-выражения для каждого интерфейса

Реализуем небольшое приложение — список пользователей. Каждый пользователь будет представлен классом User:

public class User {
    private final String name;
    private final int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() { return name; }
    public int getAge() { return age; }

    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

Создадим список пользователей:

List<User> users = List.of(
    new User("Анна", 23),
    new User("Борис", 17),
    new User("Вика", 31),
    new User("Гоша", 15)
);

Predicate: фильтрация взрослых

Predicate<User> isAdult = user -> user.getAge() >= 18;
List<User> adults = users.stream()
    .filter(isAdult)
    .toList();

System.out.println("Взрослые: " + adults); // Взрослые: [Анна (23), Вика (31)]

Consumer: печать пользователей

Consumer<User> printUser = user -> System.out.println("Пользователь: " + user);
adults.forEach(printUser);

Supplier: генерация пользователей

Supplier<User> randomUserSupplier = () -> {
    String[] names = {"Дима", "Катя", "Лёша"};
    int randomAge = 10 + (int)(Math.random() * 30);
    String randomName = names[(int)(Math.random() * names.length)];
    return new User(randomName, randomAge);
};

User randomUser = randomUserSupplier.get();
System.out.println("Случайный пользователь: " + randomUser);

Function: получение имени пользователя

Function<User, String> getName = user -> user.getName();
List<String> names = users.stream()
    .map(getName)
    .toList();

System.out.println("Имена: " + names); // Имена: [Анна, Борис, Вика, Гоша]

5. Полезные нюансы

Использование в Stream API: filter, map, forEach и др.

Соберём всё воедино и напишем цепочку преобразований:

users.stream()
     .filter(user -> user.getAge() >= 18)         // Predicate<User>
     .map(user -> user.getName().toUpperCase())    // Function<User, String>
     .forEach(name -> System.out.println("Взрослый: " + name)); // Consumer<String>

Результат:

Взрослый: АННА
Взрослый: ВИКА

Таблица-шпаргалка: что куда подставлять

Где используется Какой интерфейс нужен Пример использования
filter (Stream)
Predicate<T>
filter(u -> u.getAge() > 18)
map (Stream)
Function<T, R>
map(u -> u.getName())
forEach (Stream, List)
Consumer<T>
forEach(u -> System.out.println(u))
generate (Stream)
Supplier<T>
Stream.generate(() -> ...)

Почему важно знать про функциональные интерфейсы?

  • Они лежат в основе всех лямбда-выражений в Java.
  • Позволяют писать универсальный, переиспользуемый и лаконичный код.
  • Упрощают работу с коллекциями, потоками, асинхронными задачами.

Когда использовать какой интерфейс?

  • Predicate — когда нужно проверить условие (фильтровать, искать).
  • Consumer — когда нужно что-то сделать с объектом (вывести, записать, отправить).
  • Supplier — когда нужно получить или сгенерировать объект (фабрики, генераторы).
  • Function — когда нужно преобразовать объект из одного типа в другой.

6. Типичные ошибки при работе с функциональными интерфейсами

Ошибка №1: Неправильный выбор интерфейса. Иногда новички путают Predicate и Function — например, пытаются вернуть boolean из Function, а не из Predicate. Запомните: Predicate всегда возвращает boolean, Function — любой другой тип.

Ошибка №2: Неиспользование стандартных интерфейсов. Часто пишут свои интерфейсы вроде «Checker» c методом boolean check(T t) вместо использования Predicate. Лучше использовать стандартные — они везде поддерживаются и делают код понятнее для других программистов.

Ошибка №3: Лямбда слишком сложная. Если лямбда превращается в мини-роман на 10 строк, её стоит вынести в отдельный метод или класс. Лямбда — это про краткость и читаемость.

Ошибка №4: Забытая аннотация @FunctionalInterface. Если вы пишете свой функциональный интерфейс — не забудьте про аннотацию. Она подстрахует от случайных ошибок (вроде добавления второго абстрактного метода).

Ошибка №5: Использование изменяемого состояния внутри лямбды. Если лямбда меняет внешние переменные или коллекции, это может привести к неожиданным багам, особенно при работе с потоками. Лучше избегать побочных эффектов.

1
Задача
JAVA 25 SELF, 49 уровень, 0 лекция
Недоступна
Анализ заголовков книг в Библиотеке Фантастики 📚
Анализ заголовков книг в Библиотеке Фантастики 📚
1
Задача
JAVA 25 SELF, 49 уровень, 0 лекция
Недоступна
Управление игровыми показателями в "Виртуальном Королевстве" 🎮
Управление игровыми показателями в "Виртуальном Королевстве" 🎮
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Andrey Уровень 1
12 октября 2025
49