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;
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ