JavaRush /Курсы /JAVA 25 SELF /Введение в лямбда-выражения

Введение в лямбда-выражения

JAVA 25 SELF
21 уровень , 0 лекция
Открыта

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) -> ...

Таблица-сводка

Посмотрим, как одна и та же идея записывается через лямбду и через анонимный класс:

Интерфейс Лямбда-выражение (пример) Эквивалент анонимного класса
Runnable
() -> System.out.println("Hi")
new Runnable() { public void run() { System.out.println("Hi"); } }
Consumer<String>
s -> System.out.println(s)
new Consumer<String>() { public void accept(String s) { System.out.println(s); } }
Comparator<Integer>
(a, b) -> a - b
new Comparator<Integer>() { public int compare(Integer a, Integer b) { return 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;
1
Задача
JAVA 25 SELF, 21 уровень, 0 лекция
Недоступна
Голосовое объявление системы 📣
Голосовое объявление системы 📣
1
Задача
JAVA 25 SELF, 21 уровень, 0 лекция
Недоступна
Стандартизация пользовательского ввода 📝
Стандартизация пользовательского ввода 📝
1
Задача
JAVA 25 SELF, 21 уровень, 0 лекция
Недоступна
Рейтинг спортсменов по "силе удара" 🥊
Рейтинг спортсменов по "силе удара" 🥊
1
Задача
JAVA 25 SELF, 21 уровень, 0 лекция
Недоступна
Динамическое формирование отчётов 📊
Динамическое формирование отчётов 📊
Комментарии (7)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
No Name Уровень 22
22 февраля 2026
А как тут return что то возвращает, метод же типа void: Раньше реализовывали так: Operation sum = new Operation() { @Override public void apply(int a, int b) { return a + b; } };
No Name Уровень 22
20 февраля 2026
Мне так кажется будто лямбда это сокращение анонимного класса
No Name Уровень 22
20 февраля 2026
Только лямбда на один метод
Zlyden' Уровень 66
20 ноября 2025
4. Многострочное тело (нужны фигурные скобки и return, если есть явный возврат): Function<Integer, Integer> square = x -> { int result = x * x; return result; }; System.out.println(square.apply(6)); // 36 Первый Integer — это тип результата функции, второй Integer — это тип параметра. Наоборот же, второй Integer - тип результата функции, а первый - тип параметра. 😡
Mike Уровень 31
11 февраля 2026
верно подмечено ;) Подвис на этой теме и решил поизучать её глубже @FunctionalInterface public interface Function<T,R> ---- Type Parameters: T - the type of the input to the function R - the type of the result of the function ---- Method Details apply R apply(T t) Applies this function to the given argument. Parameters: t - the function argument Returns: the function result
Overbyaka Уровень 29
11 февраля 2026
Да, тоже смутилась, когда в этой же лекции дают пример:

Function<String, Integer> parse = s -> Integer.parseInt(s);
System.out.println(parse.apply("123")); // 123
Andrey Уровень 1
15 сентября 2025
21