JavaRush /Курсы /JAVA 25 SELF /Передача функций как параметров: примеры

Передача функций как параметров: примеры

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

1. Передача поведения вместо данных

Зачем вообще передавать функцию как параметр?

Представьте: у вас есть метод, который сортирует список. Но как он узнает, как именно сортировать? По алфавиту? По длине? По дате рождения? Можно было бы написать отдельный метод для каждого случая, но это быстро превратится в адскую копипасту.

Вместо этого Java позволяет передавать поведение (то есть функцию или лямбду), которая определяет, как сортировать, фильтровать, преобразовывать и так далее. Это делает код гибким и переиспользуемым.

Пример: сортировка с компаратором

List<String> names = List.of("Анна", "Борис", "Вика");

// Передаём поведение: как сравнивать элементы (по длине строки)
names.stream()
     .sorted((a, b) -> a.length() - b.length())
     .forEach(System.out::println);

Пример: фильтрация с предикатом

List<String> names = List.of("Анна", "Борис", "Вика");

// Передаём поведение: кого оставить (имя длиннее 4 символов)
names.stream()
     .filter(name -> name.length() > 4)
     .forEach(System.out::println);

В обоих случаях вы не жёстко «зашили» логику в метод, а дали ему кусочек поведения, который он применяет к каждому элементу.

Польза: меньше дублирования, больше гибкости

Вместо десятков методов с похожим кодом, но разной логикой, вы пишете один универсальный метод, который принимает «что делать» в виде функции. Это экономит время, уменьшает ошибки и делает код проще для тестирования.

2. Синтаксис передачи функций

Лямбда-выражения как аргументы

Самый частый способ — прямо в месте вызова метода написать лямбду со стрелкой ->:

list.forEach(item -> System.out.println(item));

Или даже короче, если подходящий метод уже есть — ссылка на метод (::):

list.forEach(System.out::println);

Ссылки на методы (method references)

Если у вас уже есть подходящий метод, его можно передать как ссылку :::

// Обычный метод
public static boolean isLongName(String name) {
    return name.length() > 4;
}

// Передача ссылки на метод
names.stream()
     .filter(MyClass::isLongName)
     .forEach(System.out::println);

Это работает, если сигнатура метода совпадает с ожидаемой в функциональном интерфейсе (Predicate<T>, Function<T, R>, Comparator<T> и т.д.).

3. Примеры из стандартной библиотеки

Collections.sort и Comparator

List<String> names = new ArrayList<>(List.of("Анна", "Борис", "Вика"));

// Сортировка по длине имени
names.sort((a, b) -> a.length() - b.length());
System.out.println(names); // [Вика, Анна, Борис]

Stream.filter и Predicate

List<String> names = List.of("Анна", "Борис", "Вика");

// Оставить только имена, начинающиеся на 'В'
names.stream()
     .filter(name -> name.startsWith("В"))
     .forEach(System.out::println); // Вика

Stream.map и Function

List<String> names = List.of("Анна", "Борис", "Вика");

// Преобразовать имена в верхний регистр
names.stream()
     .map(String::toUpperCase)
     .forEach(System.out::println);

Optional.ifPresent и Consumer

Optional<String> opt = Optional.of("Привет!");

// Если значение есть, напечатать его
opt.ifPresent(s -> System.out.println("В строке: " + s));

4. Практика: пишем собственные методы с функциями-параметрами

Пора применить знания на практике! Давайте напишем несколько методов, которые принимают функцию в качестве параметра и используют её внутри.

Пример 1: Метод для обработки элементов списка (forEach по-своему)

Допустим, у нас есть список пользователей (User). Мы хотим выполнить над каждым пользователем какое-то действие — например, вывести имя, отправить e-mail, начислить бонусы и т.д. Вместо того чтобы жёстко «зашивать» действие, передадим его как параметр — Consumer<User>!

import java.util.List;
import java.util.function.Consumer;

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

public class UserProcessor {
    // Метод, принимающий Consumer<User>
    public static void processUsers(List<User> users, Consumer<User> action) {
        for (User user : users) {
            action.accept(user);
        }
    }

    public static void main(String[] args) {
        List<User> users = List.of(
            new User("Анна", 25),
            new User("Борис", 30),
            new User("Вика", 22)
        );

        // Вывести имена всех пользователей
        processUsers(users, user -> System.out.println("Имя: " + user.name));

        // Начислить бонус (для примера просто вывести)
        processUsers(users, user -> System.out.println(user.name + " получил бонус!"));
    }
}

Пример 2: Метод для фильтрации элементов (Predicate)

Напишем метод, который возвращает только тех пользователей, которые подходят под условие (например, только совершеннолетние):

import java.util.List;
import java.util.ArrayList;
import java.util.function.Predicate;

public class UserFilter {
    public static List<User> filterUsers(List<User> users, Predicate<User> condition) {
        List<User> result = new ArrayList<>();
        for (User user : users) {
            if (condition.test(user)) {
                result.add(user);
            }
        }
        return result;
    }

    public static void main(String[] args) {
        List<User> users = List.of(
            new User("Анна", 25),
            new User("Борис", 17),
            new User("Вика", 22)
        );

        // Фильтруем только совершеннолетних
        List<User> adults = filterUsers(users, user -> user.age >= 18);
        adults.forEach(user -> System.out.println(user.name)); // Анна, Вика
    }
}

Пример 3: Метод-преобразователь (Function)

А теперь метод, который из списка пользователей делает список их имён (или любые другие преобразования):

import java.util.List;
import java.util.ArrayList;
import java.util.function.Function;

public class UserMapper {
    public static <R> List<R> mapUsers(List<User> users, Function<User, R> mapper) {
        List<R> result = new ArrayList<>();
        for (User user : users) {
            result.add(mapper.apply(user));
        }
        return result;
    }

    public static void main(String[] args) {
        List<User> users = List.of(
            new User("Анна", 25),
            new User("Борис", 17),
            new User("Вика", 22)
        );

        // Получаем список имён
        List<String> names = mapUsers(users, user -> user.name);
        System.out.println(names); // [Анна, Борис, Вика]

        // Получаем список возрастов
        List<Integer> ages = mapUsers(users, user -> user.age);
        System.out.println(ages); // [25, 17, 22]
    }
}

Пример 4: Метод-генератор (Supplier)

Иногда нужно получить некое значение «по требованию» — например, сгенерировать случайное число, создать объект, получить текущее время. Для этого подходит интерфейс Supplier<T>.

import java.util.function.Supplier;

public class ValueGenerator {
    public static int getValue(Supplier<Integer> supplier) {
        return supplier.get();
    }

    public static void main(String[] args) {
        // Получаем случайное число
        int random = getValue(() -> (int)(Math.random() * 100));
        System.out.println("Случайное число: " + random);

        // Получаем текущее время в миллисекундах
        long time = getValue(System::currentTimeMillis);
        System.out.println("Время: " + time);
    }
}

5. Единое приложение: связываем примеры

Давайте представим, что мы разрабатываем простое приложение «Список пользователей». Мы уже умеем:

  • Фильтровать пользователей по условию (например, только совершеннолетних);
  • Преобразовывать пользователей во что-то (имена, e-mail, возраст);
  • Выполнять действия над каждым пользователем (печать, начисление бонусов);
  • Генерировать значения по требованию (например, создавать новых пользователей).

Теперь, используя все эти методы, мы можем строить гибкие сценарии обработки данных, не переписывая код каждый раз под новую задачу.

import java.util.*;
import java.util.function.*;

public class UserApp {
    static class User {
        String name;
        int age;
        User(String name, int age) {
            this.name = name;
            this.age = age;
        }
        public String toString() {
            return name + " (" + age + ")";
        }
    }

    // Универсальный метод для обработки пользователей
    static void processUsers(List<User> users, Consumer<User> action) {
        for (User user : users) action.accept(user);
    }

    // Универсальный фильтр
    static List<User> filterUsers(List<User> users, Predicate<User> condition) {
        List<User> result = new ArrayList<>();
        for (User user : users) if (condition.test(user)) result.add(user);
        return result;
    }

    // Универсальный преобразователь
    static <R> List<R> mapUsers(List<User> users, Function<User, R> mapper) {
        List<R> result = new ArrayList<>();
        for (User user : users) result.add(mapper.apply(user));
        return result;
    }

    public static void main(String[] args) {
        List<User> users = List.of(
            new User("Анна", 25),
            new User("Борис", 17),
            new User("Вика", 22)
        );

        // 1. Вывести всех пользователей
        processUsers(users, user -> System.out.println("Пользователь: " + user));

        // 2. Найти только взрослых
        List<User> adults = filterUsers(users, user -> user.age >= 18);
        System.out.println("Совершеннолетние: " + adults);

        // 3. Получить имена взрослых
        List<String> adultNames = mapUsers(adults, user -> user.name);
        System.out.println("Имена взрослых: " + adultNames);

        // 4. Начислить бонус взрослым
        processUsers(adults, user -> System.out.println(user.name + " получил бонус!"));
    }
}

Почему это лучше, чем «обычный» подход?

Такой способ даёт гибкость: одни и те же методы можно использовать в совершенно разных сценариях, просто передавая другие функции. Код при этом не разрастается в бесконечные варианты «фильтруем так», «печатаем эдак» — у нас есть общие инструменты, которые работают везде. За счёт этого он получается компактнее, понятнее и менее склонен к ошибкам. А ещё такой стиль отлично стыкуется с современным Stream API, так что при переходе к потокам не придётся учиться заново — подход тот же самый.

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

Ошибка №1: Сигнатура не совпадает с ожидаемым интерфейсом.
Если метод принимает Predicate<User>, а вы передаёте лямбду, которая возвращает не boolean, а, например, String, компилятор тут же скажет «ай-ай-ай!» и не даст собрать проект. Проверяйте, что возвращаемое значение и параметры совпадают с ожидаемыми.

Ошибка №2: Лямбда использует переменные, которые могут измениться.
В Java лямбда-выражения могут использовать только final или effectively final переменные из внешнего контекста. Если вы пытаетесь изменить такую переменную внутри лямбды — будет ошибка компиляции.

Ошибка №3: Путаница между интерфейсами.
Иногда хочется передать, например, Consumer<T>, но случайно пишете функцию, которая что-то возвращает (например, Function<T, R>). Проверьте, что ваша лямбда возвращает ровно то, что нужно (или ничего — для Consumer).

Ошибка №4: Слишком сложные лямбды прямо в параметрах.
Если лямбда занимает больше одной-двух строк, лучше вынести её в отдельную переменную или метод. Иначе код станет нечитаемым, и разбираться в нём будете только вы (и то не всегда).

1
Задача
JAVA 25 SELF, 49 уровень, 2 лекция
Недоступна
Парад Зверей: Построение по кличкам 🐘
Парад Зверей: Построение по кличкам 🐘
1
Задача
JAVA 25 SELF, 49 уровень, 2 лекция
Недоступна
Отчёт о показателях "Звёздного Мониторинга" 🔭
Отчёт о показателях "Звёздного Мониторинга" 🔭
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ