Перейти к содержимому
Теория · Лямбды и Stream API · урок 1 из 12

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

Чему вы научитесь

Чему вы научитесь:

  • передавать методу действие лямбда-выражением и читать запись w -> …;
  • пользоваться интерфейсами Function и Predicate, ссылками на методы и Comparator;
  • собирать стрим из filter, map и toList и объяснять, почему без терминальной операции он ничего не делает;
  • доставать результат из Optional без исключения.

Представьте конвейер на фабрике. По ленте едут коробки, а вдоль ленты стоят станции. Работник на станции делает одно и то же с каждой коробкой, которая к нему приехала.

Что именно делать, написано на карточке с инструкцией: «сними с ленты всё, что легче килограмма» или «наклей на каждую коробку ярлык». Сама станция не меняется. Меняется только карточка, которую ей вручили.

До сих пор методы в Java принимали значения: числа, строки, объекты. А как передать методу действие — «что сделать с каждым элементом»? В теме про интерфейсы для этого пришлось бы написать целый класс ради одной строки кода. Лямбда-выражение вручает такую карточку одной строкой:

JAVA
import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> words =
            new ArrayList<>(List.of("cat", "zebra", "ox", "horse"));
        words.removeIf(w -> w.length() < 4);
        System.out.println(words); // [zebra, horse]
        words.forEach(w -> System.out.println(w.toUpperCase()));
    }
}

Давайте разберём программу по шагам:

  1. words — изменяемый список ArrayList из четырёх слов. Из списка List.of удалять элементы нельзя, поэтому слова копируются в ArrayList.
  2. words.removeIf(w -> w.length() < 4) — метод removeIf сам проходит по списку, как станция по ленте. Карточка w -> w.length() < 4 говорит, что делать с каждым словом w: удалить, если в нём меньше четырёх букв.
  3. System.out.println(words) печатает [zebra, horse]: слова cat и ox удалены.
  4. words.forEach(...) тоже проходит по списку и для каждого слова выполняет свою карточку. Она печатает слово заглавными буквами, поэтому выводятся ZEBRA и HORSE, каждое на своей строке.

Как записывают лямбду

Лямбда-выражение — короткая запись действия: слева от стрелки -> параметры, справа тело. Запись w -> w.length() < 4 читается так: «возьми w и ответь, правда ли, что в нём меньше четырёх букв».

Тип параметра писать не нужно. Компилятор знает, что words — список строк, значит, и w — строка String. Имя параметра выбираете вы, как имя переменной.

Один параметр

Если параметр один, скобки вокруг него не ставят: w -> w.length() < 4. Тело из одного выражения не требует return: лямбда сама возвращает его значение.

Ноль или несколько параметров

Без параметров и с несколькими параметрами скобки обязательны. Лямбда () -> "hello" ничего не принимает и возвращает строку, а (a, b) -> a + b принимает два значения и возвращает их сумму.

Тело из нескольких строк

Если действию нужно несколько строк, тело берут в фигурные скобки, как тело метода:

JAVA
String letter = "z";
words.forEach(w -> {
    if (w.contains(letter)) {
        System.out.println("found " + w); // found zebra
    }
});

Внутри фигурных скобок каждая инструкция заканчивается точкой с запятой. Карточка для forEach ничего не возвращает. Лямбде, которая должна вернуть значение, в таком теле нужен явный return.

Функциональный интерфейс

Какой же тип у самой карточки? Вспомните тему про интерфейсы: переменная типа интерфейса принимала объект любого класса, который его реализует. С лямбдой так же: Java делает из неё объект, и этот объект реализует интерфейс с одним методом. Функциональный интерфейс — интерфейс, в котором ровно один абстрактный метод. Напишем свой:

JAVA
public class Main {
    public static void main(String[] args) {
        Rule isShort = w -> w.length() < 4;
        System.out.println(isShort.check("ox"));    // true
        System.out.println(isShort.check("horse")); // false
    }
}

interface Rule {
    boolean check(String w);
}

Разберём по строкам:

  1. interface Rule объявляет один метод без тела — check: принять строку и вернуть boolean. Других абстрактных методов нет, значит, интерфейс функциональный.
  2. Rule isShort = w -> w.length() < 4; — лямбда становится объектом типа Rule, а её тело — телом метода check.
  3. isShort.check("ox") выполняет тело лямбды для "ox" и печатает true, а для "horse" — false.

По единственному методу компилятор понимает, какие у лямбды параметры и что она возвращает. Поэтому лямбду можно написать только там, где ждут функциональный интерфейс. removeIf и forEach тоже ждут такие интерфейсы — готовые, из стандартной библиотеки. Над своим интерфейсом можно поставить @FunctionalInterface, и компилятор проверит, что метод без тела в нём ровно один.

На собеседовании часто спрашивают, что такое функциональный интерфейс. Ответ: интерфейс ровно с одним абстрактным методом, который и реализует лямбда.

Здесь картинка с карточкой неточна. Бумажную карточку читает человек и может понять её по-своему. Лямбда — объект, и станция вызывает его единственный метод: больше она с ним ничего делать не умеет.

Лямбда видит переменные метода

В примере с letter лямбда пользуется переменной, объявленной снаружи, в методе main. Так можно, но с условием: эту переменную нельзя менять после того, как она получила значение. Переменная, которой больше ничего не присваивают, называется фактически финальной: слова final у неё нет, но ведёт она себя как final.

Зачем такое правило? Лямбду можно сохранить в переменной и выполнить позже, поэтому Java копирует значение переменной внутрь лямбды. Так мастер вписывает число в карточку в тот момент, когда отдаёт её станции. Если бы переменную потом поменяли, карточка и переменная разошлись бы. Поэтому Java запрещает менять такую переменную и внутри лямбды, и снаружи.

Напишите внутри лямбды count++ для локальной переменной count, и javac ответит:

TEXT
error: local variables referenced from a lambda expression must be final or effectively final

Почему переменная в лямбде должна быть фактически финальной — частый вопрос на собеседовании.

Частая ошибка

Внутри лямбды переменную не меняли, а изменили ниже:

JAVA
int min = 4;
words.removeIf(w -> w.length() < min); // ✗
min = 5;

Программа не компилируется, и javac показывает на строку с лямбдой то же сообщение: local variables referenced from a lambda expression must be final or effectively final. Виновата строка ниже: присваивание min = 5 в любом месте метода делает min не фактически финальной. Если нужен другой порог, заведите для него новую переменную.

Главное

  • Лямбда-выражение параметры -> тело — короткая запись действия, которое метод выполнит сам, например для каждого элемента списка.
  • Лямбда становится объектом функционального интерфейса — интерфейса ровно с одним абстрактным методом.
  • Скобки вокруг параметров нужны, когда параметр не один; тело из нескольких строк берут в фигурные скобки и возвращают значение через return.
  • Лямбда может пользоваться локальной переменной, только если та фактически финальная: после первого значения ей ничего не присваивают.