1. Лямбда-вирази та колекції
Згадаймо, як виглядала обробка колекцій до появи лямбда-виразів. Припустімо, у нас є список рядків, і ми хочемо вивести їх на екран:
List<String> list = Arrays.asList("кіт", "пес", "їжак");
for (String s : list) {
System.out.println(s);
}
Усе просто, але якщо ми хочемо, наприклад, видалити зі списку всі порожні рядки, доводиться писати цикл з умовою, а інколи й використовувати ітератор (інакше буде ConcurrentModificationException). Або, скажімо, сортування за довжиною рядка:
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
Навіть для такої простої задачі — уже 5 рядків коду і купа «шумних» дужок. Як оптимізувати? Відповідь ви вже знаєте: лямбда-вирази.
Застосування лямбда-виразів у методах колекцій
Починаючи з Java 8, інтерфейси колекцій отримали нові методи, які приймають функціональні інтерфейси — а отже, ми можемо передавати туди лямбда-вирази. Ось найпопулярніші з них:
- forEach(Consumer<T> action)
- removeIf(Predicate<T> filter)
- sort(Comparator<T> c)
- replaceAll(UnaryOperator<T> operator)
Приклад: forEach
Вивести всі елементи списку на екран (старий спосіб):
for (String s : list) {
System.out.println(s);
}
Тепер — з лямбдою:
list.forEach(s -> System.out.println(s));
Або ще коротше, якщо хочеться погратися в «гуру Java»:
list.forEach(System.out::println); // method reference, розберемо пізніше
Приклад: removeIf
Видалити всі порожні рядки зі списку:
List<String> animals = new ArrayList<>(Arrays.asList("кіт", "", "пес", "їжак", ""));
animals.removeIf(s -> s.isEmpty());
System.out.println(animals); // [кіт, пес, їжак]
Приклад: sort
Сортування списку за довжиною рядка:
List<String> animals = Arrays.asList("кіт", "пес", "їжак", "слон");
animals.sort((a, b) -> a.length() - b.length());
System.out.println(animals); // [кіт, пес, слон, їжак]
Приклад: replaceAll
Перетворити всі рядки у верхній регістр:
List<String> animals = new ArrayList<>(Arrays.asList("кіт", "пес", "їжак"));
animals.replaceAll(s -> s.toUpperCase());
System.out.println(animals); // [КІТ, ПЕС, ЇЖАК]
2. Stream API та лямбда-вирази
Із виходом Java 8 з’явився Stream API — потужний інструмент для обробки колекцій у функціональному стилі. Потоки дозволяють фільтрувати, перетворювати, сортувати, збирати колекції за допомогою ланцюжків методів. І всі ці методи приймають лямбда-вирази!
Важливо: Повний розбір Stream API буде пізніше, зараз — лише базові приклади для розуміння ролі лямбд.
Приклад: фільтрація
Залишити лише рядки довші за 3 символи:
List<String> animals = Arrays.asList("кіт", "слон", "їжак", "крокодил");
animals.stream()
.filter(s -> s.length() > 3)
.forEach(System.out::println); // слон, їжак, крокодил
Приклад: перетворення (map)
Зробити всі рядки великими літерами:
List<String> animals = Arrays.asList("кіт", "слон", "їжак");
List<String> upper = animals.stream()
.map(s -> s.toUpperCase())
.collect(Collectors.toList());
System.out.println(upper); // [КІТ, СЛОН, ЇЖАК]
Приклад: сортування
Отримати відсортований за довжиною список (не змінюючи вихідний):
List<String> animals = Arrays.asList("кіт", "слон", "їжак", "крокодил");
List<String> sorted = animals.stream()
.sorted((a, b) -> a.length() - b.length())
.collect(Collectors.toList());
System.out.println(sorted); // [кіт, слон, їжак, крокодил]
3. Порівняння з анонімними класами
Порівняймо, як виглядав би той самий код з анонімним класом і з лямбдою.
Сортування за довжиною рядка
Анонімний клас:
animals.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
Лямбда-вираз:
animals.sort((a, b) -> a.length() - b.length());
Висновок:
Лямбда-вираз заощаджує купу рядків і робить код більш читабельним. Менше дужок, менше шуму — більше суті!
Видалення порожніх рядків
Анонімний клас:
animals.removeIf(new Predicate<String>() {
@Override
public boolean test(String s) {
return s.isEmpty();
}
});
Лямбда-вираз:
animals.removeIf(s -> s.isEmpty());
4. Практика: короткі задачі з лямбда-виразами
Спробуймо на практиці застосувати лямбда-вирази в мініпрограмі для роботи зі списком користувачів.
Приклад 1: Фільтрація користувачів за віком
class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return name + " (" + age + ")";
}
}
List<User> users = Arrays.asList(
new User("Аліса", 17),
new User("Боб", 25),
new User("Чарлі", 15)
);
users.stream()
.filter(u -> u.age >= 18)
.forEach(System.out::println); // Боб (25)
Приклад 2: Сортування користувачів за ім’ям
List<User> users = Arrays.asList(
new User("Аліса", 17),
new User("Боб", 25),
new User("Чарлі", 15)
);
users.sort((u1, u2) -> u1.name.compareTo(u2.name));
System.out.println(users);
// [Аліса (17), Боб (25), Чарлі (15)]
Приклад 3: Перетворення списку користувачів на список імен
List<String> names = users.stream()
.map(u -> u.name)
.collect(Collectors.toList());
System.out.println(names); // [Аліса, Боб, Чарлі]
Приклад 4: Видалити всіх неповнолітніх
List<User> users = new ArrayList<>(Arrays.asList(
new User("Аліса", 17),
new User("Боб", 25),
new User("Чарлі", 15)
));
users.removeIf(u -> u.age < 18);
System.out.println(users); // [Боб (25)]
5. Корисні нюанси
Особливості: область видимості та «ефективно final» змінні
Лямбда-вираз може використовувати змінні із зовнішнього методу, але тільки якщо вони final або «ефективно final» (тобто не змінюються після ініціалізації).
int minAge = 18;
users.stream()
.filter(u -> u.age >= minAge)
.forEach(System.out::println);
Якщо ви спробуєте змінити minAge після використання в лямбді — компілятор видасть помилку.
Таблиця: основні методи колекцій і стрімів зі лямбда-виразами
| Метод колекції/стріму | Що робить | Тип лямбда-виразу | Приклад |
|---|---|---|---|
|
Для кожного елемента | |
|
|
Видаляє елементи за умовою | |
|
|
Сортує елементи | |
|
|
Замінює кожен елемент | |
|
|
Фільтрує потік | |
|
|
Перетворює елементи | |
|
|
Обхід потоку | |
|
|
Сортування в потоці | |
|
7. Типові помилки
Помилка № 1: Лямбда занадто довга. Якщо всередині лямбда-виразу у вас уже 5 рядків коду, умови, цикли й try-catch — найімовірніше, варто винести цей код в окремий метод. Лямбди гарні для короткої логіки.
Помилка № 2: Використання змінних, які змінюються. Якщо ви намагаєтеся всередині лямбди змінити змінну із зовнішнього методу (наприклад, лічильник), компілятор цього не дозволить. Змінна має бути final або ефективно final.
Помилка № 3: Забуваєте, що методи колекцій/стрімів не завжди змінюють вихідну колекцію. Наприклад, stream().filter(...) не змінює вихідний список, а повертає новий потік. Якщо хочете отримати колекцію — використовуйте collect(Collectors.toList()).
Помилка № 4: Лямбда-вираз не підходить за типом. Якщо метод приймає, наприклад, Comparator<T>, а ви намагаєтеся передати лямбду з одним параметром (а не двома) — буде помилка компіляції.
Помилка № 5: Втрачаєте читаність при вкладених лямбдах. Якщо у вас ланцюжок із map, filter, forEach, і всередині кожної лямбди ще одна лямбда — код стає нечитабельним. У таких випадках краще розбити вирази на окремі кроки або винести частини в методи.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ