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: Надто складні лямбди прямо в параметрах.
Якщо лямбда займає більше однієї–двох рядків, краще винести її в окрему змінну або метод. Інакше код стане нечитабельним, і розбиратися в ньому будете лише ви (і то не завжди).
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ