Лямбда-выражения
Чему вы научитесь:
- передавать методу действие лямбда-выражением и читать запись
w -> …; - пользоваться интерфейсами
FunctionиPredicate, ссылками на методы иComparator; - собирать стрим из
filter,mapиtoListи объяснять, почему без терминальной операции он ничего не делает; - доставать результат из
Optionalбез исключения.
Представьте конвейер на фабрике. По ленте едут коробки, а вдоль ленты стоят станции. Работник на станции делает одно и то же с каждой коробкой, которая к нему приехала.
Что именно делать, написано на карточке с инструкцией: «сними с ленты всё, что легче килограмма» или «наклей на каждую коробку ярлык». Сама станция не меняется. Меняется только карточка, которую ей вручили.
До сих пор методы в 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()));
}
}Давайте разберём программу по шагам:
words— изменяемый списокArrayListиз четырёх слов. Из спискаList.ofудалять элементы нельзя, поэтому слова копируются вArrayList.words.removeIf(w -> w.length() < 4)— методremoveIfсам проходит по списку, как станция по ленте. Карточкаw -> w.length() < 4говорит, что делать с каждым словомw: удалить, если в нём меньше четырёх букв.System.out.println(words)печатает[zebra, horse]: словаcatиoxудалены.words.forEach(...)тоже проходит по списку и для каждого слова выполняет свою карточку. Она печатает слово заглавными буквами, поэтому выводятсяZEBRAиHORSE, каждое на своей строке.
Как записывают лямбду
Лямбда-выражение — короткая запись действия: слева от стрелки -> параметры, справа тело. Запись w -> w.length() < 4 читается так: «возьми w и ответь, правда ли, что в нём меньше четырёх букв».
Тип параметра писать не нужно. Компилятор знает, что words — список строк, значит, и w — строка String. Имя параметра выбираете вы, как имя переменной.
Один параметр
Если параметр один, скобки вокруг него не ставят: w -> w.length() < 4. Тело из одного выражения не требует return: лямбда сама возвращает его значение.
Ноль или несколько параметров
Без параметров и с несколькими параметрами скобки обязательны. Лямбда () -> "hello" ничего не принимает и возвращает строку, а (a, b) -> a + b принимает два значения и возвращает их сумму.
Тело из нескольких строк
Если действию нужно несколько строк, тело берут в фигурные скобки, как тело метода:
String letter = "z";
words.forEach(w -> {
if (w.contains(letter)) {
System.out.println("found " + w); // found zebra
}
});Внутри фигурных скобок каждая инструкция заканчивается точкой с запятой. Карточка для forEach ничего не возвращает. Лямбде, которая должна вернуть значение, в таком теле нужен явный return.
Функциональный интерфейс
Какой же тип у самой карточки? Вспомните тему про интерфейсы: переменная типа интерфейса принимала объект любого класса, который его реализует. С лямбдой так же: 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);
}Разберём по строкам:
interface Ruleобъявляет один метод без тела —check: принять строку и вернутьboolean. Других абстрактных методов нет, значит, интерфейс функциональный.Rule isShort = w -> w.length() < 4;— лямбда становится объектом типаRule, а её тело — телом методаcheck.isShort.check("ox")выполняет тело лямбды для"ox"и печатаетtrue, а для"horse"—false.
По единственному методу компилятор понимает, какие у лямбды параметры и что она возвращает. Поэтому лямбду можно написать только там, где ждут функциональный интерфейс. removeIf и forEach тоже ждут такие интерфейсы — готовые, из стандартной библиотеки. Над своим интерфейсом можно поставить @FunctionalInterface, и компилятор проверит, что метод без тела в нём ровно один.
На собеседовании часто спрашивают, что такое функциональный интерфейс. Ответ: интерфейс ровно с одним абстрактным методом, который и реализует лямбда.
Здесь картинка с карточкой неточна. Бумажную карточку читает человек и может понять её по-своему. Лямбда — объект, и станция вызывает его единственный метод: больше она с ним ничего делать не умеет.
Лямбда видит переменные метода
В примере с letter лямбда пользуется переменной, объявленной снаружи, в методе main. Так можно, но с условием: эту переменную нельзя менять после того, как она получила значение. Переменная, которой больше ничего не присваивают, называется фактически финальной: слова final у неё нет, но ведёт она себя как final.
Зачем такое правило? Лямбду можно сохранить в переменной и выполнить позже, поэтому Java копирует значение переменной внутрь лямбды. Так мастер вписывает число в карточку в тот момент, когда отдаёт её станции. Если бы переменную потом поменяли, карточка и переменная разошлись бы. Поэтому Java запрещает менять такую переменную и внутри лямбды, и снаружи.
Напишите внутри лямбды count++ для локальной переменной count, и javac ответит:
error: local variables referenced from a lambda expression must be final or effectively finalПочему переменная в лямбде должна быть фактически финальной — частый вопрос на собеседовании.
Частая ошибка
Внутри лямбды переменную не меняли, а изменили ниже:
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. - Лямбда может пользоваться локальной переменной, только если та фактически финальная: после первого значения ей ничего не присваивают.