Java передача параметров в лямбду по замыканию

Опубликовано: 04.05.2024

Среди новшеств, которые были привнесены в язык Java с выходом JDK 8, особняком стоят лямбда-выражения. Лямбда представляет набор инструкций, которые можно выделить в отдельную переменную и затем многократно вызвать в различных местах программы.

Основу лямбда-выражения составляет лямбда-оператор , который представляет стрелку -> . Этот оператор разделяет лямбда-выражение на две части: левая часть содержит список параметров выражения, а правая собственно представляет тело лямбда-выражения, где выполняются все действия.

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

В роли функционального интерфейса выступает интерфейс Operationable , в котором определен один метод без реализации - метод calculate . Данный метод принимает два параметра - целых числа, и возвращает некоторое целое число.

По факту лямбда-выражения являются в некотором роде сокращенной формой внутренних анонимных классов, которые ранее применялись в Java. В частности, предыдущий пример мы можем переписать следующим образом:

Чтобы объявить и использовать лямбда-выражение, основная программа разбивается на ряд этапов:

Определение ссылки на функциональный интерфейс:

Причем параметры лямбда-выражения соответствуют параметрам единственного метода интерфейса Operationable, а результат соответствует возвращаемому результату метода интерфейса. При этом нам не надо использовать ключевое слово return для возврата результата из лямбда-выражения.

Так, в методе интерфейса оба параметра представляют тип int , значит, в теле лямбда-выражения мы можем применить к ним сложение. Результат сложения также представляет тип int , объект которого возвращается методом интерфейса.

Использование лямбда-выражения в виде вызова метода интерфейса:

Так как в лямбда-выражении определена операция сложения параметров, результатом метода будет сумма чисел 10 и 20.

При этом для одного функционального интерфейса мы можем определить множество лямбда-выражений. Например:

Отложенное выполнение

Одним из ключевых моментов в использовании лямбд является отложенное выполнение (deferred execution). То есть мы определяем в одном месте программы лямбда-выражение и затем можем его вызывать при необходимости неопределенное количество раз в различных частях программы. Отложенное выполнение может потребоваться, к примеру, в следующих случаях:

Выполнение кода отдельном потоке

Выполнение одного и того же кода несколько раз

Выполнение кода в результате какого-то события

Выполнение кода только в том случае, когда он действительно необходим и если он необходим

Передача параметров в лямбда-выражение

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

Если метод не принимает никаких параметров, то пишутся пустые скобки, например:

Если метод принимает только один параметр, то скобки можно опустить:

Терминальные лямбда-выражения

Выше мы рассмотрели лямбда-выражения, которые возвращают определенное значение. Но также могут быть и терминальные лямбды, которые не возвращают никакого значения. Например:

Лямбды и локальные переменные

Лямбда-выражение может использовать переменные, которые объявлены во вне в более общей области видимости - на уровне класса или метода, в котором лямбда-выражение определено. Однако в зависимости от того, как и где определены переменные, могут различаться способы их использования в лямбдах. Рассмотрим первый пример - использования переменных уровня класса:

Переменные x и y объявлены на уровне класса, и в лямбда-выражении мы их можем получить и даже изменить. Так, в данном случае после выполнения выражения изменяется значение переменной x.

Теперь рассмотрим другой пример - локальные переменные на уровне метода:

Локальные переменные уровня метода мы также можем использовать в лямбдах, но изменять их значение нельзя. Если мы попробуем это сделать, то среда разработки (Netbeans) может нам высветить ошибку и то, что такую переменную надо пометить с помощью ключевого слова final , то есть сделать константой: final int n=70; . Однако это необязательно.

Более того, мы не сможем изменить значение переменной, которая используется в лямбда-выражении, вне этого выражения. То есть даже если такая переменная не объявлена как константа, по сути она является константой.

Блоки кода в лямбда-выражениях

Существуют два типа лямбда-выражений: однострочное выражение и блок кода. Примеры однострочных выражений демонстрировались выше. Блочные выражения обрамляются фигурными скобками. В блочных лямбда-выражениях можно использовать внутренние вложенные блоки, циклы, конструкции if, switch, создавать переменные и т.д. Если блочное лямбда-выражение должно возвращать значение, то явным образом применяется оператор return :

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

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

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

Лямбда-выражения должны использоваться для захвата значений, а не переменных. Захват значений побуждает писать код без побочных эффектов, поскольку альтернатива труднее.

Что понимается под захватом значений в лямбда-выражении?

Это значит, что в лямбда-выражениях стоит использовать внешние (относительно выражения) неизменяемые значения, а не внешние переменные, значение и внутреннее состояние которых могут меняться. Под внешними неизменяемыми значениями, соответственно, подразумеваются effectively final локальные переменные и поля примитивных типов, а также effectively final объекты, внутреннее состояние которых не будет меняться.

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

Проблема с использованием переменной ( counter ) вместо значения видна в таком примере:

Рассчитывать на то, что на экран будет выведено значение 100000 , не приходится, потому что налицо race condition. В моём тесте этот код смог получить правильное значение только в 299 случаях из 100 тысяч.

Это одна из причин почему локальные переменные, используемые в лямбда-выражении, должны быть effectively final. Допустимость кода

Привела бы к race condition для локальной переменной, что стало бы новым витком проблем в многопоточном программировании на Java. Локальные переменные считаются потокобезопасными, и ломать этот принцип разработчикам Java не хотелось.

Можно "обдурить" компилятор в плане ограничения на effectively final значение таким образом:

Так что "выстрелить себе в ногу" при использовании effectively final локальной переменной всё же можно. Конечно, не стоит удивляться тому, что значение опять-таки будет посчитано неправильно. На практике так делать определённо не стоит.

Да, здесь можно использовать AtomicInteger :

Однако это убивает всю идею распараллеливания кода.

В данном случае предполагается использование связки из map и reduce :

Часть с map и reduce можно записать и так:

Статью Brian Goetz (автора книги "Java Concurrency in Practice") по этому поводу можно прочитать здесь.

Однако проблемы при захвате переменных вместо значений могут возникать не только при параллельном выполнении. Например:

В данном коде происходит захват переменной (не effectively final поля) x , из-за чего вместо ожидаемого вывода

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

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

В нашем случае код с лямбда-выражением будет выглядеть так, что вместо четырех методов появится один, принимающий условие как аргумент:

А уж при вызове метода мы будем передавать лямбда-выражение:

Выше мы получили все электронные адреса.

Выглядит страшно, поэтому распространение получили такие вещи с Java 8 именно благодаря сахару.

В аргументе getEmail() мы передаем интерфейс Predicate. Но как компилятор определяет, какой именно метод интерфейса передается?

Немного о функциональных интерфейсах

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

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

Допишем остальные методы

Допишем вызовы с лямбда-выражениями оставшихся двух методов.

Этот получает электронные адреса из группы1:

Подробнее о функциональных интерфейсах

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

Вот примеры, угадайте является ли следующий интерфейс функциональным:

Ответ нет, в интерфейсе Movable нет ни одного абстрактного метода, а надо один.

Ответ да, в интерфейсе Runnable ровно один метод (из Movable не наследуется ни одного).

Интерфейс FastRunnable функциональный, поскольку он наследует один метод из Runnable, а default-метод fastrun() не учитывается.

Интерфейс Swimming не является функциональным, поскольку из Movable методы не наследуются, а метод ss() статический, а значит не учитывается.

Подробнее о синтаксисе лямбда-выражения

Вернемся к примеру с электронными адресами. В нем среди прочих мы использовали такое лямбда-выражение:

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

Слева от стрелки параметры, а справа тело метода.

Параметр метода здесь один, он имеет тип String. Возвращаемое значение тоже имеет тип String.

Это значит, что данное лямбда-выражение подойдет к любому функциональному интерфейсу, имеющему метод с одним параметром типа String и возвращающим значение такого же типа. Например, вместо встроенного в JDK 8 интерфейса Predicate мы могли бы определить и использовать любой собственный интерфейс:

Название тут не важно, лишь бы было совпадение по количеству и типам аргументов.

Когда можно опустить круглые скобки

Рассмотрим внимательнее левую часть двух вышеприведенных выражений:

Краткая форма

Краткая формаё

А это полная форма того же лямбда-выражения:


Полная форма

Во втором выражении присутствует тип аргумента и круглые скобки, а в первом нет.

Это означает, что например , если метод без аргументов, то круглые скобки должны присутствовать.

Вот примеры корректно написанных лямбда-выпажений:

А вот примеры некорректных лямбда-выражений:

  1. В первом выражении задан тип, а значит нужны скобки.
  2. Во втором аргументов два, а не один, что означает, снова нужны скобки.
  3. В третьем снова два аргумента, а не один.

Фигурные скобки справа

Теперь сравним правую часть выражений, во втором выражении есть фигурные скобки <>.

Фигурные скобки дают возможность написать в правой части более одной строки кода. Но при этом нельзя опускать точку с запятой и оператор return, именно этому они во втором выражении и есть.

Как уже понятно, в первом выражении все это опущено потому, что одна строка кода позволяет все это опустить.

Приведем еще примеры корректный лямбда-выражений:

Найдите ошибки в лямбда-выражениях

А теперь некорректные, угадайте, что с ними не так:

Привильный вариант такой:

А почему некорректны вот эти выражения, еще не упоминалось:

Ошибка в том, что если хотя бы один тип параметра слева указан, то остальные тоже должны быть указаны.

Чтобы сделать выражения корректными, надо либо убрать все типы (компилятор обычно их может определить из кода, не переживайте), либо, наоборот, указать все типы:

Вот еще некорретное выражение:

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

Вот так будет правильно:

Ссылки на методы (Method references)

Но это еще не все, синтаксический сахар простирается дальше.

На всякий случай вот код метода стандартного функционального интерфейса Consumer из jdk, в нем один параметр и возвращаемый тип void:

Записи (1) и (2) эквивалентны.

Стандартные функциональные интерфейсы и примеры их использования в JDK 8

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

В пакете java.util.function их целых 43.

Вот эти шесть интерфейсов (в третьей колонке примеры с method reference, который мы рассматривали выше):

Интерфейс Сигнатура Пример из JDK
UnaryOperator T apply(T t) String::toLowerCase
BinaryOperator T apply(T t1, T t2) BigInteger::add
Predicate boolean test(T t) Collection::isEmpty
Function R apply(T t) Arrays::asList
Supplier T get() Instant::now
Consumer void accept(T t) System.out::println

А название из первого поясняет суть той или иной сигнатуры.

А вот (почти) все интерфейсы из java.util.function, которые поместились на скриншот:

java.utl.function
java.util.function

Проще зайти и посмотреть их сигнатуру, чем перечислять тут. Но они производятся по таким принципам:

Замыкания в Java

К удивлению, результат в консоли будет таким:

На первый взгляд кажется магией, ведь локальные переменные в методах живут только во время метода, а потом забываются. А тут arr[] определенно запоминается.

Но на самом деле arr[] находится не в методе, а в синтетическом final поле некоторого класса, который генерируется jvm и содержит нашу функцию () -> ++arr[0]; Так что все нормально.

Итоги

Пока итогов нет, статья будет дополняться. Исходный код некоторых примеров доступен на GitHub.


lambda

Cреди прочих нововведений в примере Hello,Streams был показан код, фильтрующий коллекцию:

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

Синтаксис может быть упрощён: если в коде ровным счётом одно выражение, фигурные скобки вокруг кода могут быть опущены:

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

В большинстве случаев тип параметров так же может быть определён автоматически и его можно пропустить:

Причём если параметр ровно один, можно опустить и скобки. Однако, для создания лямбда-выражения без параметров скобки необходимы:

Лямбда-выражения может быть использовано там, где ожидается функциональный интерфейс, то есть интерфейс, реализующий единственный абстрактный метод. Тут надо отметить, что в Java8 появились интерфейсы, содержащие в себе код (и, следовательно, множественное наследование). Да и в ранних версиях языка можно было переопределять такие методы, как toString ( ) прямо в интерфейсе.

Аннотация @FunctionalInterface говорит компилятору о ваших намерениях и позволяет проверить интерфейс на соответствие требованиям на этапе компиляции. Функциональный интерфейс можно использовать в качестве типа переменной, передаваемой в функцию:

При это фактически параметром функции выступает некоторое лямбда-выражение, совместимое с единственным методом интерфейса по типу:

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

В Java8 имеется некоторое количество предопределённых функциональных интерфейсов, которые находятся в пакете java.util.function. Например, там определён интерфейс Predicate <T> , который ожидает метод filter ( ) . Использование стандартных функциональных интерфейсов позволяет отказаться от доморощенного интерфейса, из примера выше, и тем самым упростить код и сделать его более читабельным:

Обратите внимание, что хоть интерфейс и изменился, лямбда-выражение осталось тем же самым.

Лямбда-выражения имеют доступ к переменным области видимости, в которой их определили:

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

С другой стороны, в java8 есть замыкания, то есть лямбда-выражение может использовать переменные уже после того, как действие их области видимости закончилось:

Очевидно, что к моменту использования лямбда-выражения локальная переменная length уже вышла из своей области видимости и превратилась в тыкву, но её значение осталось запомненным в конкретном экземпляре лямбда-выражения.

Ссылки на методы позволяют отказаться даже от написания лямбда-выражений и просто указывать какой метод следует вызвать там, где ожидается лямбда-выражение. Синтаксис крайне прост:

В данном коде String :: compareToIgnoreCase эквивалентно лямбда-выражению ( o , str ) -> o . compareToIgnoreCase ( str )

Аналогичным образом можно ссылаться на статические методы класса или на методы конкретного экземпляра класса:

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

Так же можно ссылаться на методы объектов this или super , а так же на конструкторы Class :: new . Последняя возможность широко используется при работе с потоковыми данными, которая будет описана в отдельной статье.

Передача лямбда-выражений в качестве аргументов в Java

Один из таких контекстов возни­кает при передаче лямбда-выражения в качестве аргумента.

В действительностипередача лямбда-выражений в качестве аргументов является весьма распростра­ненным примером их применения.

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

Благадоря этому значительно повышается выразительная сила Java.

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

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

В следующем примере программы демонстрируется весь этот процесс:

Ниже приведен результат выполнения данной программы:

$ javac lambdarg . java

Прежде всего обратите внимание в данном примере программы на метод stringOp(), у которого имеются два параметра.

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

А второй параметр метода, stringOp(), относится к типу String и обозначает обрабатываемую сим­вольную строку.

Затем обратите внимание на первый вызов метода stringOp():

где в качестве аргумента данному методу передается простое лямбда-выражение. При этом создается экземпляр функционального интерфейса StringFunc и ссыл­ка на данный объект передается первому параметру метода stringOp().

Таким образом, код лямбда-выражения, встраиваемый в экземпляр класса, передается данному методу. Контекст целевого типа лямбда-выражения определяется типом его параметра.

А поскольку лямбда-выражение совместимо с этим типом, то рас­сматриваемый здесь вызов достоверен.

Встраивать в метод такие простые лямбда­в-ыражения, как упомянутое выше, нередко оказывается очень удобно, особенно когда лямбда-выражение предназначается для однократного употребления.

Далее в рассматриваемом здесь примере программы методу stringOp() пере­дается блочное лямбда-выражение.

Оно удаляет пробелы из исходной символьной строки и еще раз показано ниже.

И хотя здесь указывается блочное лямбда-выражение, описанный выше про­цесс передачи лямбда-выражения остается тем же самым и для простого одиноч­ного лямбда-выражения.

Но в данном случае некоторым программистам синтак­сис может показаться несколько неуклюжим.

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

И тогда остается только передать эту ссылку вызываемому методу. Такой прием показан в конце рассматриваемого здесь примера программы, где определяется блочное лямбда-выражение изменяющее порядок следования символов в строке на обратный.

Это лямбда-вы­ражение присваивается переменной reverse, ссылающейся на функциональны йинтерфейс StringFunc.

Следовательно, переменную reverse можно передатьв качестве аргумента первому параметру метода stringOp().

Именно так и делает­ся в конце данной программы, где методу stringOp() передаются переменная reverse и обрабатываемая символьная строка.

Экземпляр, получаемый в результате вычисления каждого лямбда-выражения, является реализацией функционального интерфейса StringFunc, поэтому каждое из этих выражений может быть передано в качестве первого аргумента вызываемому методу stringOp().

И последнее замечание: помимо инициализации переменных, присваиванияи передачи аргументов, следующие операции образуют контекст целевого типа лямбда-выражений: приведение типов, тернарная операция ?, инициализация массивов, операторы return, а также сами лямбда-выражения.

Интересное видео по теме для нашей аудитории

Читайте также: