чем можно заменить лямбда выражение в java

Разбираем лямбда-выражения в Java

От переводчика: LambdaMetafactory, пожалуй, один из самых недооценённых механизмов Java 8. Мы открыли его для себя совсем недавно, но уже по достоинству оценили его возможности. В версии 7.0 фреймворка CUBA улучшена производительность за счет отказа от рефлективных вызовов в пользу генерации лямбда выражений. Одно из применений этого механизма в нашем фреймворке — привязка обработчиков событий приложения по аннотациям, часто встречающаяся задача, аналог EventListener из Spring. Мы считаем, что знание принципов работы LambdaFactory может быть полезно во многих Java приложениях, и спешим поделиться с вами этим переводом.

В этой статье мы покажем несколько малоизвестных хитростей при работе с лямбда-выражениями в Java 8 и ограничения этих выражений. Целевая аудитория статьи — senior Java разработчики, исследователи и разработчики инструментария. Будет использоваться только публичный Java API без com.sun.* и других внутренних классов, поэтому код переносим между разными реализациями JVM.

Короткое предисловие

Например, у нас есть следующий код:

Этот код будет преобразован компилятором Java во что-то похожее на:

Инструкция invokedynamic может быть примерно представлена как вот такой Java код:

В Oracle JRE 8 metafactory динамически генерирует Java класс, используя ObjectWeb Asm, который и создает класс-реализацию функционального интерфейса. К созданному классу могут быть добавлены дополнительные поля, если лямбда-выражение захватывает внешние переменные. Этот похоже на анонимные классы Java, но есть следующие отличия:

Реализация metafactory зависит от вендора JVM и от версии

Конечно же, инструкция invokedynamic используется не только для лямбда-выражений в Java. В основном, она применяется при выполнении динамических языков в среде JVM. Движок Nashorn для исполнения JavaScript, который встроен в Java, интенсивно использует эту инструкцию.

Далее мы сфокусируемся на классе LambdaMetafactory и его возможностях. Следующий
раздел этой статьи исходит из предположения, что вы отлично понимаете как работают методы metafactory и что такое MethodHandle

Трюки с лямбда-выражениями

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

Проверяемые исключения и лямбды

Не секрет, что все функциональные интерфейсы, которые есть в Java, не поддерживают проверяемые исключения. Преимущества проверяемых исключений перед обычными — это очень давний (и до сих пор горячий) спор.

А что, если вам нужно использовать код с проверяемыми исключениями внутри лямбда-выражений в сочетании с Java Streams? Например, нужно преобразовать список строк в список URL как здесь:

В конструкторе URL(String) объявлено проверяемое исключение, таким образом, он не может быть использован напрямую в виде ссылки на метод в классе Functiion.

Вы скажете: «Нет, возможно, если использовать вот такую хитрость»:

Это грязный хак. И вот почему:

Проблема может быть решена более «легальным» способом, с использованием знания следующих фактов:

Решение — обернуть метод Callable.call в метод без секции throws :

Сначала нам нужно объявить функциональный интерфейс, в котором нет секции throws
но который сможет делегировать вызов к Callable.call :

Третье — напишем вспомогательный метод, который вызывает Callable.call без объявления исключений:

Теперь можно переписать stream без всяких проблем с проверяемыми исключениями:

Этот код скомпилируется без проблем, потому что в callUnchecked нет объявления проверяемых исключений. Более того, вызов этого метода может быть заинлайнен при помощи мономорфного инлайн кэширования, потому что это только один класс во всей JVM, который реализует интерфейс SilentOnvoker

Если реализация Callable.call выкинет исключение во время выполнения, то оно будет перехвачено вызывающей функцией без всяких проблем:

Несмотря на возможности этого метода, нужно всегда помнить про следующую рекомендацию:

Скрывайте проверяемые исключения при помощи callUnchecked только если уверены, что вызываемый код не выкинет никаких исключений

Следующий пример показывает пример такого подхода:

Полная реализация этого метода находится здесь, это часть проекта с открытым кодом SNAMP.

Работаем с Getters и Setters

Этот раздел будет полезен тем, кто пишет сериализацию/десериализацию для различных форматов данных, таких как JSON, Thrift и т.д. Более того, он может быть довольно полезен, если ваш код сильно полагается на рефлексию для Getters и Setters в JavaBeans.

Первый шаг: необходимо создать кэш для getters и setters. Класс Method из Reflection API представляет реальный getter или setter и используется в качестве ключа.
Значение кэша — динамически сконструированный функциональный интерфейс для определенного getter’а или setter’а.

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

Автоматическое приведение типов между аргументами типа Object в функциональных интерфейсах (после стирания типов) и реальными типами аргументов и возвращамого значения достигается при помощи разницы между samMethodType и instantiatedMethodType (третий и пятый аргументы метода metafactory, соответственно). Тип созданного экземпляра метода — это и есть специализация метода, который предоставляет реализацию лямбда-выражения.

В-третьих, создадим фасад для этих фабрик с поддержкой кэширования:

А теперь — время тестировать код:

Этот подход с закэшированными getters и setters можно эффективно использовать в библиотеках для сериализации/десериализации (таких, как Jackson), которые используют getters и setters во время сериализации и десериализации.

Вызовы функциональных интерфейсов с динамически сгенерированными реализациями с использованием LambdaMetaFactory значительно быстрее, чем вызовы через Java Reflection API

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

Ограничения и баги

В этом разделе мы рассмотрим некоторые баги и ограничения, связанные с лямбда-выражениями в компиляторе Java и JVM. Все эти ограничения можно воспроизвести в OpenJDK и Oracle JDK с javac версии 1.8.0_131 для Windows и Linux.

Создание лямбда-выражений из обработчиков методов

Этот код эквивалентен:

Но что, если мы заменим обработчик метода, который указывает на getValue на обработчик, который представляет getter поля:

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

Читайте также:  Чем заменить шторы на окнах в квартире

Что интересно, getter для поля работает нормально, если будем использовать MethodHandleProxies:

Нужно отметить, что MethodHandleProxies — не очень хороший способ для динамического создания лямбда-выражений, потому что этот класс просто оборачивает MethodHandle в прокси-класс и делегирует вызов InvocationHandler.invoke методу MethodHandle.invokeWithArguments. Этот подход использует Java Reflection и работает очень медленно.

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

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

Generic исключения

Но, если мы заменим лямбда-выражение анонимным классом, то код скомпилируется:

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

Ограничения типов параметризации

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

Этот код абсолютно корректный и успешно компилируется. Класс MutableInteger удовлетворяет ограничениям обобщенного типа T:

Но код упадет с исключением во время выполнения:

Этот пример демонстрирует некорректный вывод типов в компиляторе и среде исполнения.

Обработка нескольких ограничений типов generic параметров в сочетании с использованием лямбда-выражений во время компиляции и выполнения — неконсистентна

Источник

Лямбда-выражения в 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 с использованием лямбда-выражений значительно компактнее.

Лямбды, анонимные классы и обычные классы

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

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

Читайте также:  что значит sis в английском

Но в большинстве случаев, там где можно применять лямбда-выражения, например в Stream, Optional или CompletableFuture, логичнее применять именно лямбды.

Источник

Лямбда-выражения

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Источник

Лямбда-выражения в Java 8

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

Пример использования

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

Первый метод простой — выдача всех адресов. Но другие методы сложнее, например:

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

Именно здесь и можно применить лямбда-выражение — это блок кода, описывающий функцию интерфейса; функция эта передается как параметр в метод и вызывается при надобности (в нашем случае в тот момент, когда надо проверить условие см. функцию filter.test()).

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

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

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

Далее получим адреса, начинающиеся с буквы «a»:

Аналог — анонимный класс

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

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

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

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

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

Predicate — это функциональный интерфейс, что означает, что он состоит только из одного метода (не считая статических и методов по умолчанию). Этот метод и вызывается для тестирования нашего условия. Мы можем передавать непосредственно содержимое этого метода, не указывания его имени. Компилятор и так догадается, что это за метод, поскольку в интерфейсе Predicate он всего один:

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

Таким образом, компилятор понимает, что email->email.startsWith(«a») — это реализация метода test() интерфейса Predicate.

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

Читайте также:  что значит ортопедический матрас на кровать

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

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

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

А этот получает электронные адреса на букву «а» из группы1:

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

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

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

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

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

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

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

Например, Runnable — функциональный, а значит можно поставить аннотацию @FunctionalInterface без проблем:

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

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

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

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

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

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

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

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

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

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

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

Полная форма

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Мы передаем аргумент a в метод интерфейса Consumer (стандартный интерфейс из jdk) и ничего не возвращаем (void). Этот аргумент передается дальше в метод println, который тоже ничего не возвращает. Сигнатуры методов одинаковые — и вышеприведенного метода интерфейса Consumer, и метода println:

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

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

можно использовать сокращенную запись — ссылку на метод println:

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

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

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

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

Но запомнить надо всего шесть, остальные интерфейсы производные от остальных. Например, мы выше использовали интерфейс Predicate, который возвращает boolean и принимает один аргумент. А есть BiPredicate — он такой же, но принимает два аргумента.

Вот эти шесть интерфейсов (в третьей колонке примеры с 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

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

Например, BigInteger::add — бинарный оператор.

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

java.util.function

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

Замыкания в Java

Еще один интересный (но не рекомендуемый) способ использования лямбда-выражений — это замыкания. Те, кто знает JavaScript, понимают о чем речь. Возьмем пример:

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

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

Итоги

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

Источник

Библиотека с советами