lambda выражения

lambda выражения

LINE

Все программирование грубо можно свести к данным и функциям, которые эти данные изменяют. Память умеет хранить числа, а у процессора есть набор команд для преобразования этих чисел.

Вот небольшой фрагмент. У нас есть метод для нахождения суммы sum, который принимает 2 числа и возвращает их сумму. Есть другой метод sub, который возвращает разность 2-х чисел.

Метод принимает данные и преобразует их. Но что, если мы хотим передать в метод другой метод? Что то вроде такого (синтаксис неверный, программа не скомпилируется)

В метод execute мы сначала мы передаем 2 числа, а потом абстрактную функцию, которая сама принимает 2 числа и возвращает другое число. Теперь в методе main можно сделать что то вроде такого (опять же, в java так нельзя)


Если мы передадим вместо параметра function sum, получим сумму. Если sub - разность. При этом метод execute не изменится, а мы можем добавить новые функции и определить новые операции.

Как этого можно добиться? Как передать метод/функцию в другой метод/функцию как параметр? В разных языках это можно реализовать по разному. Например, в javascript функция - это объект, поэтому ее можно передавать просто по имени. В си есть указатели, а значит мы можем создать указатель на функцию, т.е. передать как параметр адрес первой инструкции функции.

В Java нет способа передать метод как параметр, но параметром может быть объект, а у объектов есть методы.

Попробуем создать класс Func(сокращение function) с одним методом sum, который вычисляет сумму 2-х чисел.

Теперь наш метод execute принимает последним параметром объект типа Func, и вызывает метод sum объекта Func. Метод sum принимает 2 числа (a, b) и возвращает их сумму.

Если мы хотим поменять метод - мы меняем его в классе Func, например можем реализовать его как разность. Единственное, что изменив название метода нам придется поменять вызывающий метод в execute, поэтому изменим название метода класса Func на общий calculate.

Теперь метод execute будет всегда оставаться одним и тем же

Меняя реализацию класса Func мы будем менять поведение программы. Например, мы можем реализовать метод calculate любым образом

Главное, что результат программы зависит только от класса Func и не зависит от метода execute. Точнее говоря, результат зависит от метода calculate.

Теперь соберем всю картину целиком. Мы хотим иметь метод, который принимает 2 числа(a и b) и некоторый другой метод, который преобразует переданные числа a и b и возвращает результат. Передать метод в чистом виде мы не можем, а потому создаем класс и определяем в нем метод, который хотели бы передать. Объект Func в таком случае можно рассматривать как "обертку" для метода calculate. Сам объект нужен лишь для того, чтобы передать метод. После того, как мы передали объект Func мы можем вызвать его метод calculate и передать параметры a и b. Таким образом мы смогли передать метод в другой метод.

Но теперь нам приходится каждый раз менять метод calculate. Если бы этот метод находился в другой библиотеке, мы бы не имели возможности его редактировать. Поэтому лучшим решением будет использовать не конкретный класс, а интерфейс Func c абстрактным методов calculate. Любой, кто захочет использовать метод execute создаст свой класс, наследующий от Func, определит метод calculate и передаст полученный объект в метод execute.

Теперь мы можем создать отдельный класс, представляющий сумму, разность и т.д.

А затем вызывать execute следующим образом

Переменная sum в строке 2 нужна только для того, чтобы передать ее как параметр в строке 3. Больше эта переменная не нужна, поэтому мы можем сразу подставить new Sum() вместо sum и не создавать временную переменную.

Более того, нам даже не нужен отдельный класс Sum. В Java есть несколько разных типов классов, в том числе анонимный. Мы можем определить переменную типа интерфейса и создать анонимный класс, реализовав недостающие методы. Выглядит это так

Создать экземпляр интерфейса нельзя, т.к. интерфейс не реализует все методы. Но если укажем реализацию методов, можем создать класс без имени (Func - это имя типа переменной, а у самого класса имени нет). Тогда мы можем использовать такой класс, чтобы сразу создать и передать его в метод execute

Теперь внимательно посмотрим на все что мы сделали, чтобы передать метод в execute. Мы создали отдельный класс, определили метод calculate, реализовали этот метод. Все это лишь для того, чтобы передать метод.

Давайте представим себя на месте компилятора. Он точно знает, какой тип должен прийти в метод execute (Func). Он точно знает, что у этого типа есть только один метод (calculate). Он точно знает, что этот метод принимает 2 числа и возвращает 1 число. Значит, все это (строки с 2 по 8) можно записать короче:

  • что ожидает получить метод calculate
  • что он должен сделать
  • что он должен вернуть

Таким образом выражение

можно заменить на

Мы убираем весь шаблонный код по созданию экземпляра, переопределению метода и оставляем только то, что нам нужно - сам метод, который принимает 2 числа и возвращает их сумму.

Последняя запись и называется лямбдой.

Как теперь будет выглядеть передача в метод execute

Коротко и ясно: взять а и b и вернуть их сумму.

Устройство лямбды

Лямбда выражения состоят из 3-х частей

  • параметры лямбды
  • стрелка
  • тело лямбды

Типы параметров можно опускать (почти всегда компилятор сможет вывести типы). Тело лямбда выражения может состоять из простого выражения, или быть заключено в фигурные скобки, тогда чтобы вернуть тип отличный от void потребуется явно указать return "значение".

Особенности лямбда выражений

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

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

Компилятор сможет вывести необходимые типы для лямбды только в том случае, если у интерфейса есть только один абстрактный метод. Такие интерфейсы довольно распространены в Java, и для них есть отдельное название - функциональный интерфейс. Для таких интерфейсов есть специальный маркер - аннотация @FunctionalInterface.

Вот пример функционального интерфейса Comparator. Хотя в нем больше одного метода, абстрактный только один - compare.

Основные типы лямбд

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

Consumer

Частым примером метода является метод, который принимает 1 параметр и ничего не возвращает (возвращает void). Такой метод как бы "потребляет" параметр, поэтому он consumer.

Для примера можно посмотреть на println. Этот метод принимает строку и ничего не возвращает. Поэтому мы можем передать этот метод как тело лямбды.

Supplier

Supplier противоположен consumer, он наоборот создает, или "поставляет" объект.

Это соответствует методу, который ничего не принимает, но возвращает объект.

Function

Function соответствует методу, который принимает объект одного типа и возвращает объект другого типа.

Примером может быть метод, который принимает строку и возвращает ее размер

Predicate

Predicate служит для одной цели - ответить на вопрос да или нет. Если метод называется filter, то скорее всего он принимает именно Predicate.

Это соответствует методу, который принимает объект и возвращает boolean. Например метод, проверяющий число на четность.

UnaryOperator

UnaryOperator представляет из себя метод, который принимает параметр и возвращает параметр одного типа.

Да, если посмотреть на этот интерфейс - это Function у которого оба параметра одного типа T. Примером может быть метод строки, который возвращает строку в верхнем регистре (но тоже строку)

BinaryOperator

BinaryOperator - это метод, который принимает 2 параметра и возвращает 1 параметр. Все параметры одного типа.

BinaryOperator наследуется от BiFunction - это тот же Function, но который принимает 2 параметра, а не 1. Примером BinaryOperator может служить метод add у BigInteger

Остальные типы лямбд

Остальные лямбда выражения это в основном надстройки над приведенными выше. Например, LongConsumer - это Consumer, который принимает long. BooleanSupplier - это Supplier, который возвращает boolean. LongToDoubleFunction - это Function, который принимает long и возвращает double. Все эти лямбды перечислены в пакете java.util.function.

Заключение

Лямбда выражение можно назвать анонимной функцией. Это особая запись, которая кратко и ясно выражает намерения: что взять, что сделать и что вернуть. В частности в Java (лямбды появились в версии 8) лямбды смогли заменить анонимные классы (особенно слушатели событий), сделав при этом код более простым и понятным.

Все то, что делают лямбды можно сделать и без них, потратив немного больше времени. С этой точки зрения лямбды просто синтаксический сахар.

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

Report Page