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 строк, его сложно читать. В таких случаях лучше вынести логику в отдельный метод.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ