Лямбда-выражения в Java — что это такое, зачем нужны и как выглядят
Лямбда-выражения или анонимные функции встречаются во многих языках программирования. Рассказываем про лямбда-выражения в Java с примерами.
Лямбда-выражения встречаются в разных языках программирования: JavaScript, PHP, C# и других. В этой статье поговорим о лямбда-выражениях в Java.
Лямбда-выражения или анонимные функции — это блоки кода с параметрами, которые можно вызвать из другого места программы. Они называются анонимными, потому что в отличие от функций, у них нет имён. Слово «лямбда» пришло из лямбда-исчисления, которое придумал профессор Алонзо Чёрч. Он использовал греческую букву лямбда (λ), чтобы отметить параметры.
Лямбда-выражения в Java
- Присутствуют начиная с 8 версии.
- Являются анонимными классами, реализующими метод функционального интерфейса.
- Имеют доступ только к final (или effectively final) переменным из охватывающей области видимости (для потокобезопасности).
- Не могут возвращать значение в каких-то ветках, а в других не возвращать.
- Позволяют уменьшить количество кода и повысить его читаемость.
Примеры синтаксиса лямбда-выражений в Java
Лямбда-выражения в Java состоят из параметров и стрелки —> отделяющей тело функции. Скобки нужны, если параметров 0 или больше одного. Для однострочных лямбд ключевое слово return не обязательно.
(список параметров) -> тело лямбды
Без параметров
@FunctionalInterface interface MyFunctionalInterface < //метод без параметров public String sayHello(); >public class Example < public static void main(String args[]) < // лямбда выражение MyFunctionalInterface msg = () ->< return "Привет мир"; >; System.out.println(msg.sayHello()); > >
Этот код выведет в консоль текст: «Привет мир!».
C параметром
import java.awt.*; public class ButtonListenerNewWay < public static void main(String[] args) < Frame frame=new Frame("ActionListener java8"); Button b=new Button("Click Here"); b.setBounds(50,100,80,50); b.addActionListener(e ->System.out.println("Привет мир!")); frame.add(b); frame.setSize(200,200); frame.setLayout(null); frame.setVisible(true); > >
В этом примере лямбда-выражение используется для обработки нажатия кнопки.
С несколькими параметрами
interface StringConcat < public String sconcat(String a, String b); >public class Example < public static void main(String args[]) < StringConcat s = (str1, str2) ->str1 + str2; System.out.println("Result: "+s.sconcat("Hello ", "World")); > >
Здесь лямбда-выражение склеивает строки.
Лямбда-выражения в Java
Поддержка лямбда-выражений, реализованная в Java 8, стала одним из наиболее значимых нововведений за последнее время. Будучи упрощённой записью анонимных классов, лямбды позволяют писать более лаконичный код при работе со Stream или Optional. Лямбда-выражения часто используются как совместно со многими API стандартной библиотеки Java, так и со сторонними API, среди которых JavaFX, реактивные стримы и т.д.
Лямбды и функциональные интерфейсы
Лямбда-выражение или просто лямбда в Java — упрощённая запись анонимного класса, реализующего функциональный интерфейс.
Функциональный интерфейс в Java — интерфейс, в котором объявлен только один абстрактный метод. Однако, методов по умолчанию (default) такой интерфейс может содержать сколько угодно, что можно видеть на примере java.util.function.Function. Функциональный интерфейс может быть отмечен аннотацией @FunctionalInterface, но это не обязательное условие, так как JVM считает функциональным любой интерфейс с одним абстрактным методом.
Пример простого функционального интерфейса:
Структура лямбда-выражения
Сигнатура лямбда-выражения соответствует сигнатуре абстрактного метода реализуемого функционального интерфейса. Можно даже сказать, что лямбда-выражение является реализацией абстрактного метода этого функционального интерфейса. Главное отличие сигнатуры лямбда-выражения от сигнатуры метода в том, что она состоит только из двух частей: списка аргументов и тела, разделённых при помощи «->». Возвращаемый тип и возможные выбрасываемые исключения JVM берёт из интерфейса.
Типы аргументов лямбда-выражения опциональны, так как они декларируются интерфейсом, но при использовании обобщений (дженериков) с extends/super может возникнуть необходимость в указании конкретных типов аргументов. При этом стоит отметить, что типы либо указываются для всех аргументов, либо не указываются вообще. Это же касается и использования var, введённой в Java 11. Всё это можно свести к такому правилу: все аргументы объявляются либо с типами, либо с var, либо без них.
Если у лямбда-выражения всего один аргумент, и для него не требуется объявление типа или var, то круглые скобки можно опустить. В остальных случаях, в том числе если лямбда не принимает никаких аргументов, скобки нельзя опустить.
Аналогичная ситуация и с телом лямбда-выражений: если оно состоит только из одной строки, то фигурные скобки, точку с запятой (;) и директиву return можно тоже опустить.
В качестве тела лямбда-выражения может использоваться ссылка на метод.
Создание лямбда-выражений
Допустим, нам нужна реализация CarFilter, описанного выше, которая проверяла бы, что автомобиль выпущен не раньше 2010 года. Если мы будем использовать анонимный класс, то создание объекта CarFilter будет выглядеть примерно следующим образом:
Но мы можем описать объект CarFilter при помощи лямбда-выражения:
Однако, эту запись можно сделать ещё меньше:
Согласитесь, что такая запись зачительно меньше и лаконичнее, чем использование анонимного класса.
Применение лямбда-выражений
Допустим у нас есть задача написать метод, выводящий из полученного списка автомобили, у которых тип кузова (body) — STATION_WAGON и мощность (power) — больше 200 л.с.
Скорее всего, мы напишем что-то вроде:
В целом, если нам требуется всего один подобный метод, то этот код можно оставить без изменений и даже не задумываться об использовании лямбда-выражений. Но, допустим, у нас появляется задача реализовать ещё один метод, который бы выводил все автомобили, у которых кузов не PICKUP_TRUCK, или метод, который бы сохранял в БД все автомобили с мощностью двигателя более 150 л.с.
В этом случае логично было бы использовать сразу два функциональных интерфейса: java.util.function.Predicate — для фильтрации и java.util.function.Consumer — для действия, применяемого к подходящим объектам.
java.util.function.Predicate декларирует абстрактный метод test, который принимает объект и возвращает значение типа boolean в зависимости от соответствия переданного объекта требуемым критериям.
java.util.function.Consumer декларирует абстрактный метод accept, который принимает объект и выполняет над ним требуемые действия.
Метод printCars превратится во что-то похожее на следующий метод:
И первоначальную задачу вывести из полученного списка автомобили, у которых тип кузова (body) — STATION_WAGON и мощность (power) — больше 200 л.с. мы решили бы следующим вызовом метода processCars с использованием лямбда-выражений:
Или при помощи анонимных классов:
Вариант вызова метода processCars с использованием лямбда-выражений значительно компактнее.
Лямбды, анонимные классы и обычные классы
Как уже было написано, лямбда-выражения могут заменить анонимные классы, которые реализуют функциональные интерфейсы, но в остальных случаях анонимные классы не теряют актуальности.
Если одно и то же лямбда-выражение (или анонимный класс) используется в нескольких случаях, то появляется смысл сделать его членом класса или объекта, или и вовсе написать полноценный класс, реализующий необходимый интерфейс.
Но в большинстве случаев, там где можно применять лямбда-выражения, например в Stream, Optional или CompletableFuture, логичнее применять именно лямбды.
Полезные ссылки
- Документация по лямбда-выражениям
- Документация по анонимным классам
Лямбда-выражения
Среди новшеств, которые были привнесены в язык Java с выходом JDK 8, особняком стоят лямбда-выражения. Лямбда представляет набор инструкций, которые можно выделить в отдельную переменную и затем многократно вызвать в различных местах программы.
Основу лямбда-выражения составляет лямбда-оператор , который представляет стрелку -> . Этот оператор разделяет лямбда-выражение на две части: левая часть содержит список параметров выражения, а правая собственно представляет тело лямбда-выражения, где выполняются все действия.
Лямбда-выражение не выполняется само по себе, а образует реализацию метода, определенного в функциональном интерфейсе . При этом важно, что функциональный интерфейс должен содержать только один единственный метод без реализации.
public class LambdaApp < public static void main(String[] args) < Operationable operation; operation = (x,y)->x+y; int result = operation.calculate(10, 20); System.out.println(result); //30 > > interface Operationable
В роли функционального интерфейса выступает интерфейс Operationable , в котором определен один метод без реализации — метод calculate . Данный метод принимает два параметра — целых числа, и возвращает некоторое целое число.
По факту лямбда-выражения являются в некотором роде сокращенной формой внутренних анонимных классов, которые ранее применялись в Java. В частности, предыдущий пример мы можем переписать следующим образом:
public class LambdaApp < public static void main(String[] args) < Operationable op = new Operationable()< public int calculate(int x, int y)< return x + y; >>; int z = op.calculate(20, 10); System.out.println(z); // 30 > > interface Operationable
Чтобы объявить и использовать лямбда-выражение, основная программа разбивается на ряд этапов:
-
Определение ссылки на функциональный интерфейс:
Operationable operation;
operation = (x,y)->x+y;
int result = operation.calculate(10, 20);
При этом для одного функционального интерфейса мы можем определить множество лямбда-выражений. Например:
Operationable operation1 = (int x, int y)-> x + y; Operationable operation2 = (int x, int y)-> x - y; Operationable operation3 = (int x, int y)-> x * y; System.out.println(operation1.calculate(20, 10)); //30 System.out.println(operation2.calculate(20, 10)); //10 System.out.println(operation3.calculate(20, 10)); //200
Отложенное выполнение
Одним из ключевых моментов в использовании лямбд является отложенное выполнение (deferred execution). То есть мы определяем в одном месте программы лямбда-выражение и затем можем его вызывать при необходимости неопределенное количество раз в различных частях программы. Отложенное выполнение может потребоваться, к примеру, в следующих случаях:
- Выполнение кода отдельном потоке
- Выполнение одного и того же кода несколько раз
- Выполнение кода в результате какого-то события
- Выполнение кода только в том случае, когда он действительно необходим и если он необходим
Передача параметров в лямбда-выражение
Параметры лямбда-выражения должны соответствовать по типу параметрам метода из функционального интерфейса. При написании самого лямбда-выражения тип параметров писать необязательно, хотя в принципе это можно сделать, например:
operation = (int x, int y)->x+y;
Если метод не принимает никаких параметров, то пишутся пустые скобки, например:
()-> 30 + 20;
Если метод принимает только один параметр, то скобки можно опустить:
n-> n * n;
Терминальные лямбда-выражения
Выше мы рассмотрели лямбда-выражения, которые возвращают определенное значение. Но также могут быть и терминальные лямбды, которые не возвращают никакого значения. Например:
interface Printable < void print(String s); >public class LambdaApp < public static void main(String[] args) < Printable printer = s->System.out.println(s); printer.print("Hello Java!"); > >
Лямбды и локальные переменные
Лямбда-выражение может использовать переменные, которые объявлены во вне в более общей области видимости — на уровне класса или метода, в котором лямбда-выражение определено. Однако в зависимости от того, как и где определены переменные, могут различаться способы их использования в лямбдах. Рассмотрим первый пример — использования переменных уровня класса:
public class LambdaApp < static int x = 10; static int y = 20; public static void main(String[] args) < Operation op = ()->< x=30; return x+y; >; System.out.println(op.calculate()); // 50 System.out.println(x); // 30 — значение x изменилось > > interface Operation
Переменные x и y объявлены на уровне класса, и в лямбда-выражении мы их можем получить и даже изменить. Так, в данном случае после выполнения выражения изменяется значение переменной x.
Теперь рассмотрим другой пример — локальные переменные на уровне метода:
public static void main(String[] args) < int n=70; int m=30; Operation op = ()->< //n=100; - так нельзя сделать return m+n; >; // n=100; - так тоже нельзя System.out.println(op.calculate()); // 100 >
Локальные переменные уровня метода мы также можем использовать в лямбдах, но изменять их значение нельзя. Если мы попробуем это сделать, то среда разработки (Netbeans) может нам высветить ошибку и то, что такую переменную надо пометить с помощью ключевого слова final , то есть сделать константой: final int n=70; . Однако это необязательно.
Более того, мы не сможем изменить значение переменной, которая используется в лямбда-выражении, вне этого выражения. То есть даже если такая переменная не объявлена как константа, по сути она является константой.
Блоки кода в лямбда-выражениях
Существуют два типа лямбда-выражений: однострочное выражение и блок кода. Примеры однострочных выражений демонстрировались выше. Блочные выражения обрамляются фигурными скобками. В блочных лямбда-выражениях можно использовать внутренние вложенные блоки, циклы, конструкции if, switch, создавать переменные и т.д. Если блочное лямбда-выражение должно возвращать значение, то явным образом применяется оператор return :
Operationable operation = (int x, int y)-> < if(y==0) return 0; else return x/y; >; System.out.println(operation.calculate(20, 10)); //2 System.out.println(operation.calculate(20, 0)); //0
Обобщенный функциональный интерфейс
Функциональный интерфейс может быть обобщенным, однако в лямбда-выражении использование обобщений не допускается. В этом случае нам надо типизировать объект интерфейса определенным типом, который потом будет применяться в лямбда-выражении. Например:
public class LambdaApp < public static void main(String[] args) < Operationableoperation1 = (x, y)-> x + y; Operationable operation2 = (x, y) -> x + y; System.out.println(operation1.calculate(20, 10)); //30 System.out.println(operation2.calculate(«20», «10»)); //2010 > > interface Operationable
Таким образом, при объявлении лямбда-выражения ему уже известно, какой тип параметры будут представлять и какой тип они будут возвращать.
Лямбды — Java: Коллекции
Лямбда-функции — это анонимные функции, которые можно сохранять и передавать как обычные переменные.
Введение
Общий вид лямбды (параметры) -> выражение или (параметры) -> . Стрелка — это лямбда-оператор.
Чаще всего используются три функциональных интерфейса:
- Predicate — принимает параметр и возвращает логическое значение
- Consumer — который принимает параметр и не возвращает никакого значения
- Function — принимает параметр и возвращает какое-то значение
// Допустим, нам нужно вывести на экран // список домашних животных и вернуть только имена питомцев, // отсортированные по алфавиту ListMapString, String>> animals = List.of( Map.of("name", "Spooky", "animal", "dog"), Map.of("name", "Tom", "animal", "cat"), Map.of("name", "Jimmy", "animal", "bird"), Map.of("name", "Buddy", "animal", "dog"), Map.of("name", "Elsa", "animal", "dog"), Map.of("name", "Murka", "animal", "cat") );
// Создадим метод getPetNames() в классе AppExample // Для этого будем использовать Stream API и лямбды class AppExample public static ListString> getPetNames(ListMapString, String>> animals, String animal) return animals.stream() // Сначала отфильтруем всех питомцев, например только собак // Метод filter принимает в качестве параметра Predicate // Лямбда принимает вид питомца и возвращает true, если оно совпадает с требуемым .filter(pet -> pet.get("animal").equals(animal)) // Затем сделаем отображение, оставим только имя питомца // Метод map принимает на вход Function // Она принимает текущий элемент стрима и возвращает новый измененный .map(pet -> pet.get("name")) // Сортируем имена // Метод sorted принимает на вход Function // Лямбда принимает два соседних элемента стрима и возвращает 1, 0 или -1 // в зависимости от того, больше ли первый элемент чем второй, равен или меньше .sorted((name1, name2) -> name1.compareTo(name2)) .collect(Collectors.toList()); > >
Открыть доступ
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно
- 130 курсов, 2000+ часов теории
- 1000 практических заданий в браузере
- 360 000 студентов
Наши выпускники работают в компаниях: