Lambda ·
‹ Предыдущий Следующий ›
⏱ 5 минут чтения Обновлено: 2026-08-17

Лямбда-выражения в Java

Такой код не скомпилируется, хотя выглядит абсолютно нормально:

int count = 0;
cars.forEach(car -> count++); // error: local variables referenced from
                              // a lambda expression must be final or effectively final

Лямбда похожа на обычный блок кода внутри метода, но живёт по своим правилам. Разберёмся, что такое лямбда-выражения, как они устроены и где их применяют.

Лямбда-выражение в Java — это компактная запись анонимной функции, которую можно передать в метод как аргумент или сохранить в переменную. Лямбда-выражения появились в Java 8 (2014) и остаются одной из базовых возможностей языка во всех последующих релизах, включая современные LTS-версии. Они позволяют писать код в функциональном стиле: вместо объекта с методом передаётся само поведение.

1. Зачем нужны лямбда-выражения

Все примеры ниже работают с простым классом автомобиля:

public enum CarTypes {
    COMPACT, SPORT, SUV
}

public class Car {
    private final CarTypes type;
    private final int cost;

    public Car(CarTypes type, int cost) {
        this.type = type;
        this.cost = cost;
    }

    public CarTypes getType() {
        return type;
    }

    public int getCost() {
        return cost;
    }
}

Рассмотрим метод, который подсчитывает количество только компактных автомобилей:

private int getCompactCarsNumber(Car[] cars) {
    int result = 0;
    for (Car car : cars) {
        if (car.getType().equals(CarTypes.COMPACT)) {
            result++;
        }
    }
    return result;
}

Предположим, добавляется ещё одна задача — подсчитать количество автомобилей, у которых стоимость более 20 000:

private int getExpensiveCarsNumber(Car[] cars) {
    int result = 0;
    for (Car car : cars) {
        if (car.getCost() > 20000) {
            result++;
        }
    }
    return result;
}

Два метода отличаются ровно одной строкой — условием отбора. Вынесем это условие в отдельный интерфейс, чтобы не писать новый метод под каждый новый критерий:

public interface Searchable {
    boolean test(Car car);
}
public class CompactCarSearch implements Searchable {
    public boolean test(Car car) {
        return car.getType().equals(CarTypes.COMPACT);
    }
}
public class ExpensiveCarSearch implements Searchable {
    public boolean test(Car car) {
        return car.getCost() > 20000;
    }
}
public class CarDemo2 {
    public static void main(String[] args) {
        Car[] cars = {
                new Car(CarTypes.COMPACT, 34000),
                new Car(CarTypes.SPORT, 44000),
                new Car(CarTypes.COMPACT, 14000),
                new Car(CarTypes.COMPACT, 10000),
        };

        CarDemo2 carDemo = new CarDemo2();
        System.out.println(carDemo.getCarsNumber(cars, new CompactCarSearch()));
        System.out.println(carDemo.getCarsNumber(cars, new ExpensiveCarSearch()));

        System.out.println(carDemo.getCarsNumber(cars, new Searchable() {
            @Override
            public boolean test(Car car) {
                return car.getType().equals(CarTypes.COMPACT);
            }
        }));
    }

    private int getCarsNumber(Car[] cars, Searchable s) {
        int result = 0;
        for (Car car : cars) {
            if (s.test(car)) {
                result++;
            }
        }
        return result;
    }
}

В классе CarDemo2 шесть строк анонимного класса

carDemo.getCarsNumber(cars, new Searchable() {
    @Override
    public boolean test(Car car) {
        return car.getType().equals(CarTypes.COMPACT);
    }
})

сжимаются в одну строку лямбда-выражения:

carDemo.getCarsNumber(cars, car -> car.getType() == CarTypes.COMPACT);

Логика осталась той же, но исчез весь служебный обвес: имя класса, ключевое слово new, аннотация @Override, сигнатура метода. Компилятор сам понимает, что от него хотят, потому что у интерфейса Searchable ровно один абстрактный метод.

Мелочь, которая часто встречается в чужом коде

Константы перечислений корректнее сравнивать через ==, а не через equals(): значения enum существуют в единственном экземпляре, а == дополнительно безопасен при null слева. Вариант car.getType().equals(CarTypes.COMPACT) рабочий, но бросит NullPointerException, если тип не задан.

2. Синтаксис лямбда-выражения

Общая форма записи:

(параметры) -> тело

Например:

(Object arg1, Object arg2) -> arg1.equals(arg2)

Лямбда-выражение состоит из трёх частей: список параметров, стрелка (->) и тело. Тело бывает двух видов: выражение (без фигурных скобок, его значение возвращается автоматически) или блок (в фигурных скобках, с явным return, если метод возвращает значение).

Основные правила записи:

  • Лямбда-выражение может принимать ноль и более параметров:
    (int a1, int a2) -> { return a1 - a2; }
    (String s) -> { System.out.println(s); }
    () -> 89
  • Тип параметра можно указать явно или доверить компилятору — он выведет его из целевого типа, то есть из сигнатуры того функционального интерфейса, куда лямбда передаётся:
    (String s) -> { System.out.println(s); }
    можно переписать так:
    (s) -> { System.out.println(s); }
  • Если параметров нет или их больше одного, круглые скобки обязательны:
    (a1, a2) -> a1 + a2
    (int a1, int a2) -> a1 + a2
    () -> 42
  • Единственный параметр можно писать без скобок, но тогда нельзя указать его тип явно:
    a1 -> 2 * a1
  • Если тело состоит из одного выражения, фигурные скобки и return не нужны — значение возвращается само:
    () -> 4
    (int a) -> a * 6
  • Если тело содержит несколько инструкций, фигурные скобки обязательны, а для возврата значения нужен явный return:
    () -> {
        System.out.println("Hi");
        return 4;
    }
    
    (int a) -> {
        System.out.println(a);
        return a * 6;
    }
  • Лямбда может ничего не возвращать. Тело-блок без return и тело-выражение, результат которого отбрасывается, совместимы с void-методом функционального интерфейса:
    () -> System.out.println("Hi")
    () -> {
        System.out.println("Hi");
        return;
    }
  • Начиная с Java 11 в списке параметров можно использовать var — это удобно, когда нужна аннотация на параметре. Смешивать var с явными типами или с их отсутствием нельзя:
    (var a1, var a2) -> a1 + a2
    (@NonNull var s) -> s.trim()

Важно

Все формы выше показаны как фрагменты. Сама по себе лямбда не является выражением с самостоятельным типом: ей всегда нужен целевой тип — переменная функционального интерфейса, параметр метода или возвращаемое значение. Поэтому var f = () -> 42; не компилируется, а Supplier<Integer> f = () -> 42; — вполне.

3. Лямбда vs анонимный класс

Лямбда — это не «синтаксический сахар над анонимным классом». Различия видны и на уровне поведения, и на уровне байт-кода.

Критерий Лямбда-выражение Анонимный класс
Что означает this Объект внешнего класса Сам экземпляр анонимного класса
Что можно реализовать Только функциональный интерфейс Любой интерфейс или класс, в том числе с несколькими методами
Собственное состояние Полей нет Можно объявлять поля и блоки инициализации
Байт-код Инструкция invokedynamic, отдельный class-файл не создаётся Создаётся отдельный файл Outer$1.class
Имена переменных Нельзя объявить переменную с именем локальной переменной метода Можно затенить (shadowing) внешнюю переменную
public class ThisDemo {
    private final String name = "внешний объект";

    public void run() {
        Runnable lambda = () -> System.out.println(this.name); // "внешний объект"

        Runnable anonymous = new Runnable() {
            private final String name = "анонимный класс";

            @Override
            public void run() {
                System.out.println(this.name); // "анонимный класс"
            }
        };

        lambda.run();
        anonymous.run();
    }
}

4. На чём чаще всего ошибаются

Переменные должны быть effectively final

Лямбда захватывает значение локальной переменной, а не саму переменную. Поэтому переменная должна быть final или effectively final — фактически не изменяться после инициализации. Именно на этом падает пример из начала урока:

int count = 0;
cars.forEach(car -> count++); // ошибка компиляции

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

long count = cars.stream()
        .filter(car -> car.getType() == CarTypes.COMPACT)
        .count();

Если счётчик всё же нужен (например, в многопоточном коде), берут AtomicInteger или LongAdder: сама ссылка на объект не меняется, меняется его содержимое.

AtomicInteger counter = new AtomicInteger();
cars.forEach(car -> counter.incrementAndGet());

На поля класса это ограничение не распространяется — их лямбда может изменять свободно.

Проверяемые исключения не проходят «сквозь» лямбду

Лямбда может бросить только те исключения, которые объявлены в сигнатуре метода функционального интерфейса. У Consumer.accept() нет throws, поэтому такой код не скомпилируется:

List<String> paths = List.of("a.txt", "b.txt");
paths.forEach(p -> Files.readAllBytes(Path.of(p))); // IOException не обработан

Варианты решения: обработать исключение внутри лямбды через try/catch, обернуть его в непроверяемое (UncheckedIOException) либо объявить свой функциональный интерфейс с throws.

Лямбда с побочными эффектами в параллельном стриме

Лямбда, которая пишет во внешнюю коллекцию, в parallelStream() ведёт себя непредсказуемо: обычный ArrayList не потокобезопасен, и результат может оказаться неполным или сломанным. Собирайте результат через collect(), а не через forEach с добавлением в список.

Слишком длинная лямбда

Лямбда на пятнадцать строк теряет главное преимущество — читаемость. Вынесите тело в обычный метод и передайте ссылку на него: cars.stream().filter(CarFilters::isCheapCompact).

Вопрос с собеседования

«Почему локальная переменная в лямбде обязана быть effectively final, а поле класса — нет?» Локальные переменные живут в стеке метода и исчезают вместе с ним, поэтому лямбда получает их копию — изменять копию бессмысленно и опасно. Поля же доступны через ссылку на объект в куче, которая остаётся живой столько, сколько живёт лямбда.

5. Коротко о главном

  • Лямбда-выражение — компактная запись анонимной функции: (параметры) -> тело.
  • Лямбду можно присвоить только функциональному интерфейсу — интерфейсу с одним абстрактным методом.
  • Тип параметров выводится из целевого типа; скобки не нужны только для единственного параметра без явного типа.
  • Тело-выражение возвращает значение без return; тело-блок требует фигурных скобок и явного return, если метод не void.
  • Если лямбда просто вызывает метод, замените её ссылкой на метод.
  • Лямбды повсеместно используются в Stream API: почти каждый метод стрима принимает лямбду или ссылку на метод.
  • Захваченные локальные переменные должны быть effectively final; this внутри лямбды указывает на внешний объект.

Часто задаваемые вопросы

Лямбда-выражение — это просто сокращённая запись анонимного класса?

Нет. Внешне они решают похожую задачу, но компилируются по-разному: для анонимного класса создаётся отдельный class-файл Outer$1.class, а лямбда компилируется в инструкцию invokedynamic и связывается во время выполнения через LambdaMetafactory. Отсюда и разница в поведении: this в лямбде ссылается на внешний объект, а в анонимном классе — на сам анонимный экземпляр.

Можно ли использовать лямбда-выражения в Java 7 и раньше?

Нет. Лямбда-выражения и метод invokedynamic-связывания появились в Java 8 (2014), вместе с функциональными интерфейсами и Stream API. При компиляции с флагом --release 7 компилятор откажется принимать стрелку. До Java 8 роль лямбд выполняли анонимные классы.

Как рекурсивно вызвать лямбду из неё самой?

Напрямую не получится: локальная переменная не считается инициализированной внутри собственного инициализатора. Обходной путь — объявить лямбду полем класса или использовать массив-держатель из одного элемента. Обычно рекурсию проще оформить обычным методом и передать ссылку на него.

Замедляют ли лямбда-выражения программу?

На практике нет. Первый вызов лямбды чуть дороже из-за связывания через invokedynamic, но дальше JIT-компилятор встраивает её тело так же, как обычный вызов метода. Заметная разница возникает только при упаковке примитивов — в таком коде используйте специализированные интерфейсы вроде IntPredicate и ToIntFunction.

Как отладить лямбда-выражение?

Точку останова можно поставить прямо на строке с лямбдой — IntelliJ IDEA и Eclipse умеют останавливаться внутри её тела. Если лямбда однострочная и брейкпоинт срабатывает не там, где нужно, временно разверните её в форму с фигурными скобками или вынесите в отдельный метод. В стеке вызовов такие кадры видны как lambda$methodName$0.

Видео объяснение

Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.

Комментарии

unknown Nov 17, 2022
По-моему лямбда работают только как реализация абстрактных методов функциональных интерфейсов, но об этом ни слова((

Зарегистрируйтесь или войдите, чтобы иметь возможность оставить комментарий.