Функциональные интерфейсы и лямбда-выражения
В рамках предыдущего раздела – коллекций – мы научились хранить массивы данных так, как это принято в Java. Следующий шаг – научиться обрабатывать массивы данных так, как это принято в современной (начиная с Java 8) Java-разработке. В рамках данного раздела мы познакомимся с механизмами, которые реализуют функциональное программирование (ФП).
Но это будет в следующих уроках. Сегодня же мы познакомимся с тем, что к ФП напрямую не относится, но без чего ФП в Java никогда не стало бы популярным – познакомимся с лямбда-выражениями. По сути, синтаксическим сахаром – новым синтаксисом, который делает использование ранее существовавших механизмов более удобным.
Данный урок предлагаю построить на разборе следующего определения:
Лямбда-выражение – упрощенная форма описания анонимного класса, реализующего функциональный интерфейс.
Сразу оговорюсь, что это определение можно считать верным лишь непосредственно в Java. За пределами Java-мира оно будет выглядеть как минимум странно. Зато оно дает возможность построить удобную структуру для текущего урока.
Функциональные интерфейсы
Итак. Формулировка выше говорит об анонимном классе (с ними мы уже знакомы), реализующем функциональный интерфейс.
Функциональный интерфейс – интерфейс, имеющий лишь один абстрактный метод.
При этом такой интерфейс может содержать любое количество статических, приватных и default- методов.
Также функциональные интерфейсы, как правило, помечаются аннотацией @FunctionalInterface (указывается над самим интерфейсом). Она не является обязательной, но бросит ошибку компиляции, если помеченный ей интерфейс будет содержать более одного абстрактного метода (или не будет содержать их вовсе).
Как минимум с одним функциональным интерфейсом мы уже знакомы – это интерфейс Comparator. К слову, именно на нем будет построена часть сегодняшней практики.
В целом, функциональных интерфейсов достаточно много, но наиболее популярные мы рассмотрим в рамках статьи на metanit, ссылка будет в следующем пункте данного урока.
!NB: Как правило, функциональные интерфейсы представляют собой дженерики. Но это вовсе не обязательно. Скорее, дело в том, что для большинства задач так удобнее.
Синтаксис лямбда-выражений
Сначала предлагаю разобрать, как выглядит «описание анонимного класса, реализующего функциональный интерфейс».
Для удобства предлагаю использовать уже знакомый нам интерфейс Comparator:
Comparator<Car> numberComparator = new Comparator<>() {
@Override
public int compare(Car o1, Car o2) {
return o1.getIdentifier()
.getNumber()
.compareTo(o2.getIdentifier().getNumber());
}
};
Так выглядело создание объекта анонимного класса, реализующего Comparator, в одной из задач урока 38.
Если попытаться перевести код выше на русский язык, мы получим примерно следующее:
Переменная numberComparator является объектом анонимного класса, реализующего интерфейс Comparator, параметризованного классом Car. Анонимный класс переопределяет метод compare(), принимающий два параметра одного типа. Метод содержит следующую логику: [описание логики метода].
Наиболее интересующие нас моменты подчеркнуты. Учитывая, что интерфейс Comparator имеет лишь один абстрактный метод – название этого метода, в целом, можно опустить для лаконичности.
Используя лямбда-выражение, код выше можно описать следующим образом:
Comparator<Car> numberComparator = (o1, o2) ->
o1.getIdentifier()
.getNumber()
.compareTo(o2.getIdentifier().getNumber());
Разберемся, что есть что:
Comparator<Car> numberComparator – эта часть осталась без изменений. Переменная numberComparator, тип ссылки - интерфейс Comparator, параметризованный классом Car. Не является частью лямбда-выражения, это просто объявление переменной. Здесь оставлено для удобства.
(o1, o2) – два параметра одного типа. Типы параметров в лямбда-выражении можно указать явно, но их принято опускать – все равно они будут определены на основании сигнатуры метода, переопределением которого является лямбда-выражение.
Содержи сигнатура метода два параметра разных типов – форма записи осталась бы такой же.
Будь в исходном методе 10 параметров – в скобках перечислили бы 10 параметров, ни будь ни одного – скобки были бы пусты: ().
Если бы параметр был один – скобки можно было бы опустить: o -> …
-> – оператор лямбда-выражения. Слева от него располагаются параметры выражения (в нашем случае – (o1, o2)), после него – тело лямбда-выражения;
o1.getIdentifier()… – все то, что после «->». Олицетворяет собой [описание логики метода].
Если тело метода было представлено одной строчкой кода (одна инструкция, завершающаяся «;») – при ее представлении в виде лямбда-выражения можно опустить фигурные скобки ({}) и ключевое слово return.
Также рассмотрим то же самое лямбда-выражение, представленное в несколько инструкций (строк кода). Смысл эквивалентный, только логика описана с использованием дополнительных переменных:
Comparator<Car> numberComparator = (o1, o2) -> {
String number1 = o1.getIdentifier().getNumber();
String number2 = o2.getIdentifier().getNumber();
return number1.compareTo(number2);
};
Как видите, мы все еще имеем дело с лямбда-выражением, но его немного «разнесло». Многострочные лямбда-выражения – допустимая, но нежелательная практика. Однако на данном этапе для нас не критично.
Внутри лямбда-выражений, как и в случае с анонимными классами, можно использовать переменные/поля/методы, определенные за их пределами. Но с теми же ограничениями: значения ссылок полей и переменных изменять нельзя.
Теперь предлагаю посмотреть, насколько сократился наш код при использовании лямбда-выражения:
Comparator<Car> numberComparator = new Comparator<>() {
@Override
public int compare(Car o1, Car o2) {
return o1.getIdentifier()
.getNumber()
.compareTo(o2.getIdentifier().getNumber());
}
};
cars.sort(numberComparator);
Превратилось в
Comparator<Car> numberComparator = (o1, o2) -> o1.getIdentifier()
.getNumber()
.compareTo(o2.getIdentifier().getNumber());
cars.sort(numberComparator);
Учитывая, что лямбда-выражения редко записывают в переменные и определяют по месту вызова – можно сократить до
cars.sort((o1, o2) -> o1.getIdentifier() .getNumber() .compareTo(o2.getIdentifier().getNumber()));
!NB: конкретно в случае с Comparator подобную логику можно (и нужно) свернуть до
cars.sort(Comparator.comparing(o -> o.getIdentifier().getNumber()));
Но это не относится к теме урока.
Предлагаю рассмотреть примеры определения лямбда-выражений для различных функциональных интерфейсов в рамках следующей статьи: https://metanit.com/java/tutorial/9.3.php
Об отложенном выполнении и не только
Для начала, предлагаю ознакомиться с данной статьей: https://metanit.com/java/tutorial/9.1.php
По ряду причин я не считаю ее удачной в плане структуры, в остальном она перескажет содержание того, что мы изучили выше более подробно.
В данном пункте я предлагаю остановиться на том, что на метаните описано под пунктом «Отложенное выполнение». Именно с этим возникает большинство проблем, с которыми сталкиваются новички.
Также, как и с описанием анонимных классов, стоит помнить, что код переопределяемых методов – это именно инструкции, которые описывают поведение для объектов классов. Код, который вы пишете в методе (методах) анонимного класса, не вызовется сразу. Он будет исполнен тогда (и только тогда), когда вы вызовете у объекта этого класса конкретный метод. А лямбда выражение – это всего лишь объект анонимного класса с единственным абстрактным методом.
Обобщая, помните, что анонимный класс – это тоже класс. Но ни в коем случае не часть инструкций (кода) метода (или класса), в котором этот анонимный класс описан.
И точно также, как вы не ожидаете немедленного вызова всех методов класса String, создавая экземпляр строки, а лишь хотите видеть их запуск при прямом вызове, так и при описании анонимных классов стоит ожидать запуск его методов лишь при прямом вызове у объекта. Даже если ваш анонимный класс описан как лямбда-выражение.
Глядя на практическое применение лямбда-выражений, вы заметите, что 99% использования – это передача лямбда-выражения параметром метода. На самом деле происходит передача объекта анонимного класса, описанного как лямбда-выражение. И внутри метода обязательно будет произведен вызов метода, который вы переопределили в своем анонимном классе/лямбда-выражении.
Скажем, list.sort() внутри себя обязательно вызовет comparator.compare(). И именно при каждом таком вызове будет запускаться код, который вы описали в лямбда-выражении. При этом между описанием лямбда-выражения и использующем его list.sort() могут быть сотни строк другого кода.
Немного о декомпозиции и написании кода
Лямбда-выражения являются очень важным инструментом в современной Java-разработке. Но, к сожалению, большинство разработчиков используют лишь малую долю потенциала лямбд, даже не представляя, как использовать их более эффективно.
Ссылка ниже, надеюсь, станет для вас первым шагом к гибкому использованию лямбда-выражений.
Помните, что лямбда-выражение – это всего лишь объект анонимного класса, а значит, может быть присвоен полю, переменной, быть параметром или возвращаемым значением метода (вашего метода, а не написанного разработчиками языка или библиотеки).
Также по ссылке ниже вы найдете еще одну новую синтаксическую конструкцию – method reference – ссылка на метод. Если лямбда-выражение является способом лаконичного описания определенных анонимных классов, то ссылка на метод – это следующий шаг оптимизации кода, который укорачивает описание лямбда-выражения. Этот механизм может быть применен не к любой лямбде и имеет ряд особенностей. Несмотря на кажущуюся простоту использования, потребуются некоторые усилия, чтобы понять, как им пользоваться.
Но он, безусловно, полезен, поэтому данному механизму будет целиком посвящен один из ближайших уроков. Надеюсь, это добавит осознанности при использовании method reference в вашем коде*.
*Личный опыт автора говорит о том, что ощутимая доля Java-разработчиков использует method reference лишь тогда, когда IDEA предлагает сформировать его автоматически на основании написанного лямбда-выражения.
Собственно, обещанная ссылка: https://metanit.com/java/tutorial/9.2.php
Вместо итога
Сегодняшний урок почти полностью посвящен именно синтаксису. Поэтому я предполагаю, что для многих останется непонятной широта области применения лямбда-выражений. И это нормально. Нас впереди ждет Stream API, знакомство с Optional и методы коллекций, взаимодействующие с лямбдами (последние начнутся уже сегодня). Именно указанные темы призваны наполнить механизм лямбда-выражений смыслом. И именно они позволят раскрыть удобство использования лямбда-выражений в полной мере.
С теорией на сегодня все!

Переходим к практике:
Задача 1:
Реализуйте Задачу 1 из урока 38, описывая компараторы как лямбда-выражения.
Задача 2:
Знакомимся с функциональным интерфейсом Consumer. Используя реализацию Задачи 3 из урока 16 по ссылке, замените массив на список, а цикл for – на вызов метода forEach(), который доступен для всех наследников Iterable. Он теперь будет вашим другом и надежным соратником:)
Задача 3(*):
Реализуйте Задачу из урока 21, с использованием списка (или другой коллекции на ваш выбор). Дайте возможность искать машины по гибкому фильтру – возвращайте коллекцию машин, подходящих под конкретный фильтр (можете расширить на свой вкус):
· Номер совпадает с введенным пользователем;
· Номер содержит подстроку, указанную пользователем;
· Цвет совпадает с указанным пользователем;
· Год выпуска машины находится в диапазоне, указанном пользователем.
При этом CarService должен содержать лишь один публичный метод поиска. Можете использовать Predicate или собственный функциональный интерфейс.
Также реализуйте интерактивное меню в рамках консоли, позволяющее производить несколько поисков в рамках одного запуска программы. Предусмотрите возможность завершения программы с помощью пользовательского ввода.
Если что-то непонятно или не получается – welcome в комменты к посту или в лс:)
Канал: https://t.me/+relA0-qlUYAxZjI6
Мой тг: https://t.me/ironicMotherfucker
Дорогу осилит идущий!