1. Знайомство
Коротко: лямбда-вираз — це спосіб швидко створити реалізацію функціонального інтерфейсу «на льоту», не оголошуючи окремий клас чи анонімний клас. Це як міні-метод без імені, який можна передати як аргумент або зберегти у змінну.
Java тривалий час була класичною мовою ООП. Якщо ви хотіли передати фрагмент поведінки — наприклад, що робити під час натискання кнопки, — доводилося писати анонімні класи:
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick() {
System.out.println("Кнопку натиснуто!");
}
});
Починаючи з Java 8, з’явилися лямбда-вирази:
button.setOnClickListener(() -> System.out.println("Кнопку натиснуто!"));
Тут () -> System.out.println("Кнопку натиснуто!") — і є лямбда-вираз.
Формально: лямбда-вираз — це компактна форма запису реалізації єдиного абстрактного методу функціонального інтерфейсу.
Чому це важливо?
- Передавати поведінку як значення (наприклад, що робити з кожним елементом списку).
- Писати компактний і зрозумілий код.
- Використовувати сучасні API Java: Stream API, обробку подій, асинхронні завдання та багато іншого.
2. Синтаксис лямбда-виразів
Загальний вигляд
(параметри) -> вираз
// або
(параметри) -> { блок коду }
Приклади для різних інтерфейсів
1. Без параметрів (наприклад, Runnable):
Runnable r = () -> System.out.println("Привіт, лямбда!");
r.run();
2. Один параметр (наприклад, Consumer):
Consumer<String> printer = s -> System.out.println("Ви передали: " + s);
printer.accept("Java");
Якщо параметр лише один і його тип можна вивести, дужки можна опустити.
Тип String — це тип параметра s.
3. Кілька параметрів (наприклад, Comparator):
Comparator<Integer> cmp = (a, b) -> a - b;
System.out.println(cmp.compare(10, 5)); // 5
Тип Integer — це тип параметрів a і b.
4. Багаторядкове тіло (потрібні фігурні дужки та return, якщо є явне повернення):
Function<Integer, Integer> square = x -> {
int result = x * x;
return result;
};
System.out.println(square.apply(6)); // 36
Перший Integer — тип параметра функції, другий Integer — тип результату.
Скорочення та лаконічність
- Якщо тіло — один вираз, фігурні дужки та return можна опустити.
- Якщо параметрів немає — пишемо порожні дужки: () -> ...
- Якщо один параметр — можна без дужок: x -> ...
- Якщо параметрів більше одного — потрібні дужки: (a, b) -> ...
Підсумкова таблиця
Подивімося, як одну й ту саму ідею можна записати лямбдою або анонімним класом:
| Інтерфейс | Лямбда-вираз (приклад) | Еквівалент анонімного класу |
|---|---|---|
|
|
|
|
|
|
|
|
|
Приклад із власним інтерфейсом
Нехай маємо функціональний інтерфейс:
@FunctionalInterface
interface Operation {
int apply(int a, int b);
}
Раніше це реалізовували так:
Operation sum = new Operation() {
@Override
public void apply(int a, int b) {
return a + b;
}
};
Тепер — коротко:
Operation sum = (a, b) -> a + b;
System.out.println(sum.apply(3, 5)); // 8
Приклади для стандартних інтерфейсів
Runnable:
Runnable hello = () -> System.out.println("Hello from thread!");
new Thread(hello).start();
Comparator:
List<String> list = Arrays.asList("яблуко", "банан", "ківі");
list.sort((a, b) -> a.length() - b.length());
System.out.println(list);
Function:
Function<String, Integer> parse = s -> Integer.parseInt(s);
System.out.println(parse.apply("123")); // 123
3. Область видимості та захоплення змінних
Змінні із зовнішнього контексту (effectively final)
Лямбда-вирази можуть використовувати змінні з охоплювального методу. Але є правило: такі змінні мають бути effectively final — тобто або явно оголошені як final, або після ініціалізації не змінюватися.
Приклад:
String prefix = "Результат: ";
Function<Integer, String> f = x -> prefix + (x * 2);
// prefix тут "заморожено" — після цього його не можна змінювати
System.out.println(f.apply(5)); // Результат: 10
Якщо спробувати змінити prefix після того, як його використано у лямбді, компілятор видасть помилку.
Чому так?
Лямбда-вираз можна викликати й після виходу з методу, де оголошено змінну. Щоб уникнути непередбачуваних помилок, Java дозволяє використовувати лише незмінні локальні змінні.
Відмінність від анонімних класів
В анонімних класах діє те саме правило: змінні із зовнішнього методу мають бути final/effectively final. Проте відрізняється поведінка з областю видимості: усередині анонімного класу this посилається на сам анонімний клас, а в лямбді — на зовнішній клас.
public class Demo {
public void test() {
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println(this); // Виведе: Demo$1 (анонімний клас)
}
};
Runnable r2 = () -> System.out.println(this); // Виведе: Demo (зовнішній клас)
r1.run();
r2.run();
}
}
Лямбда і поля класу
Лямбда-вираз може звертатися до полів зовнішнього класу без додаткових обмежень:
public class Counter {
private int base = 10;
public void printSum(int x) {
Function<Integer, Integer> sum = y -> base + y + x;
System.out.println(sum.apply(5));
}
}
Тут base — поле класу, його можна змінювати.
x — параметр методу; він має бути effectively final.
4. Практика: напишемо кілька лямбда-виразів
Приклад: фільтрація списку чисел
List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5, 6);
nums.stream()
.filter(n -> n % 2 == 0)
.forEach(n -> System.out.println("Парне: " + n));
Докладніше про Stream API ви дізнаєтеся на 30-му рівні :P
Приклад: функція перетворення рядка
Function<String, String> capitalize = s -> s.toUpperCase();
System.out.println(capitalize.apply("java")); // JAVA
Приклад: власний функціональний інтерфейс
@FunctionalInterface
interface StringTransformer {
String transform(String s);
}
StringTransformer exclaim = s -> s + "!";
System.out.println(exclaim.transform("Привіт")); // Привіт!
Приклад: використання змінних із зовнішнього контексту
int factor = 2;
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.forEach(n -> System.out.println(n * factor));
// factor не можна змінити після цього!
5. Типові помилки під час роботи з лямбда-виразами
Помилка № 1: спроба змінити змінну, захоплену лямбдою.
Такий код не скомпілюється — змінна має бути effectively final:
int sum = 0;
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.forEach(n -> sum += n); // Помилка: sum не final!
Якщо потрібно накопичувати значення — використовуйте масив або об’єкт-обгортку.
Помилка № 2: плутанина з областю видимості this.
Усередині лямбда-виразу this посилається на зовнішній клас, а не на лямбду (на відміну від анонімного класу).
Помилка № 3: забули фігурні дужки та return у багаторядковому лямбда-виразі.
Якщо тіло лямбди — не один вираз, потрібні дужки та return:
Function<Integer, Integer> square = x -> {
int y = x * x;
return y;
};
Помилка № 4: некоректне визначення типу лямбда-виразу.
Лямбда-вираз завжди реалізує функціональний інтерфейс. Не можна просто написати:
var f = x -> x + 1; // Помилка! Невідомо, який тип інтерфейсу.
Потрібно явно вказати тип:
Function<Integer, Integer> f = x -> x + 1;
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ