1. Вступ
Давайте чесно: писати анонімні класи заради одного-двох рядків коду — це як наймати величезну вантажівку, щоб перевезти з пекарні до магазину одну булочку.
Наприклад, якщо ви хочете відсортувати список рядків за довжиною, до Java 8 доводилося писати ось так:
List<String> list = Arrays.asList("яблуко", "банан", "ківі");
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
Здавалося б, завдання просте, а коду — на пів екрана. Особливо дратує, коли таких операцій багато: код стає «шумним», втрачається суть задачі. Програмісти плакали, страждали, а потім придумали лямбда-вирази. Ми вже трохи їх вивчали, тепер повторимо й поглибимо наші знання.
Що таке лямбда-вираз
Лямбда-вираз — це компактна форма запису реалізації функціонального інтерфейсу, тобто інтерфейсу з єдиним абстрактним методом (наприклад, Comparator, Runnable, Consumer та багато інших).
Простіше кажучи, лямбда-вираз дозволяє вам написати функцію «на льоту», безпосередньо там, де вона потрібна, без оголошення окремого класу або методу.
Загальний синтаксис:
(параметры) -> { тело }
Приклади:
- (a, b) -> a + b — функція додавання двох чисел
- x -> x * x — функція зведення числа в квадрат
- () -> System.out.println("Hello!") — функція без параметрів
Зв’язок із функціональними інтерфейсами:
Лямбда-вираз завжди можна присвоїти змінній типу функціонального інтерфейсу або передати як аргумент у метод, який очікує такий інтерфейс.
2. Синтаксис лямбда-виразів
Без параметрів
Runnable r = () -> System.out.println("Привіт, світе!");
r.run(); // Виведе: Привіт, світе!
Один параметр
Якщо параметр один, дужки можна опустити:
Consumer<String> print = s -> System.out.println(s);
print.accept("Java — це круто!");
Кілька параметрів
Дужки обов’язкові:
Comparator<String> cmp = (a, b) -> a.length() - b.length();
Тіло з одного виразу
Якщо тіло складається з одного виразу, фігурні дужки і return не потрібні:
Function<Integer, Integer> square = x -> x * x;
System.out.println(square.apply(5)); // 25
Тіло з блоку
Якщо потрібно кілька інструкцій, використовуйте фігурні дужки і return (якщо є значення, що повертається):
Function<Integer, Integer> abs = x -> {
if (x < 0) {
return -x;
}
return x;
};
System.out.println(abs.apply(-3)); // 3
Типи параметрів
Найчастіше типи параметрів можна не вказувати — компілятор сам здогадається з контексту. Але якщо хочеться, можна й явно:
Comparator<String> cmp = (String a, String b) -> a.length() - b.length();
Лямбда без значення, що повертається
Якщо інтерфейс повертає void, просто пишіть інструкції:
list.forEach(s -> System.out.println("Елемент: " + s));
Таблиця: Варіанти синтаксису лямбда-виразів
| Що робимо | Приклад | Коментар |
|---|---|---|
| Без параметрів | |
Наприклад, для Runnable |
| Один параметр | |
Можна без дужок |
| Кілька параметрів | |
Дужки обов’язкові |
| Один вираз | |
Без return і фігурних дужок |
| Блок коду | |
З return, якщо є результат |
3. Застосування: де і як використовувати лямбда-вирази
Лямбда-вирази найчастіше використовують там, де потрібно передати «поведінку» — функцію — як аргумент. Це стало справжньою революцією для колекцій, потоків (Stream API), подій та багато іншого.
Сортування списку
До Java 8:
list.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
З лямбда-виразом:
list.sort((a, b) -> a.length() - b.length());
Створення потоку (Thread)
Thread t = new Thread(() -> System.out.println("Потік запущено!"));
t.start();
Обробка колекцій
list.forEach(s -> System.out.println(s.toUpperCase()));
Фільтрація списку
List<String> longWords = list.stream()
.filter(s -> s.length() > 5)
.collect(Collectors.toList());
Приклад: розвиваємо наш навчальний застосунок
Припустімо, у нас є список користувачів:
List<String> users = Arrays.asList("Alice", "Bob", "Charlie");
Вивести всіх користувачів, імена яких довші за 4 символи:
users.stream()
.filter(name -> name.length() > 4)
.forEach(name -> System.out.println("Користувач: " + name));
4. Область видимості змінних у лямбда-виразах
Лямбда-вирази можуть використовувати змінні з навколишнього методу, але є нюанси!
Змінні та лямбди
Лямбда в Java може «захоплювати» лише ті змінні, які після ініціалізації більше не змінюються. Якщо змінну оголошено з final — усе очевидно. Але навіть якщо слово final не написано, компілятор сам перевіряє, чи змінюється значення, чи ні. Якщо ні, то він вважає її «ніби фінальною» і спокійно допускає її в лямбду.
Приклад:
int minLength = 4; // значення ніде не змінюється
users.forEach(name -> {
if (name.length() > minLength) {
System.out.println(name);
}
});
Тут усе працює, тому що minLength залишається тим самим значенням.
Але якщо після використання в лямбді ви спробуєте переприсвоїти minLength, то отримаєте помилку компіляції:
int minLength = 4;
users.forEach(name -> {
if (name.length() > minLength) {
System.out.println(name);
}
});
minLength = 10; // Помилка! Лямбда вже "зафіксувала" значення
По суті, правило дуже просте: змінна, що потрапила в лямбду, має бути незмінною.
Відмінність від анонімних класів
В анонімних класах і лямбда-виразах змінні із зовнішнього методу працюють однаково: тільки final/ефективно final.
АЛЕ!
У лямбда-виразі this посилається на зовнішній об’єкт (поточний екземпляр класу), а в анонімному класі — на екземпляр анонімного класу. Це важливо, якщо ви всередині лямбди звертаєтеся до полів або методів поточного класу.
Приклад:
public class Example {
String name = "Зовнішній клас";
void demo() {
Runnable r1 = new Runnable() {
String name = "Анонімний клас";
@Override
public void run() {
System.out.println(this.name); // "Анонімний клас"
}
};
Runnable r2 = () -> System.out.println(this.name); // "Зовнішній клас"
r1.run();
r2.run();
}
}
5. Типові помилки під час роботи з лямбда-виразами
Помилка № 1: Використання не final/не ефективно final змінної. Якщо ви вирішили змінити змінну після того, як використали її в лямбді, компілятор одразу повідомить про помилку. Це робиться для безпеки: інакше було б незрозуміло, яке значення змінної використовувати.
Помилка № 2: Плутанина з this. У лямбда-виразі this — це зовнішній клас, а в анонімному класі — сам анонімний клас. Якщо ви намагаєтесь викликати метод зовнішнього класу з лямбди, усе працюватиме, а от із анонімного класу — ні (якщо ви розраховували на контекст зовнішнього класу).
Помилка № 3: Лямбда без контексту. Лямбда-вираз не можна використовувати сам по собі — його потрібно або присвоїти змінній функціонального інтерфейсу, або передати туди, де цей інтерфейс очікується. Спроба просто написати x -> x + 1 поза контекстом викличе помилку.
Помилка № 4: Занадто складна лямбда. Якщо лямбда-вираз стає довшим за 3–5 рядків, його складно читати. У таких випадках краще винести логіку в окремий метод.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ