Как лямбда выражение связано с анонимным классом java

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

Привет, Хабр! Представляю вашему вниманию перевод статьи «Java Lambda Expressions» автора www.programiz.com.

Введение

В этой статье, с помощью примеров, мы изучим lambda-выражения в Java, их использование с функциональными интерфейсами, параметризированными функциональными интерфейсами и Stream API.

Лямбда выражения были добавлены в Java 8. Их основная цель – повысить читабельность и уменьшить количество кода.

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

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

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

Например, интерфейс Runnable из пакета java.lang является функциональным, потому, что он содержит только один метод run().

Пример 1: объявление функционального интерфейса в java


В приведенном выше примере, интерфейс MyInterface имеет только один абстрактный метод getValue(). Значит, этот интерфейс — функциональный.

Здесь мы использовали аннотацию FunctionalInterface, которая помогает понять компилятору, что интерфейс функциональный. Следовательно, не позволяет иметь более одного абстрактного метода. Тем не менее, мы можем её опустить.

В Java 7, функциональные интерфейсы рассматривались как Single Abstract Methods (SAM). SAM обычно реализовывались с помощью анонимных классов.

Пример 2: реализация SAM с помощью анонимного класса в java


Результат выполнения:


В этом примере, мы принимаем анонимный класс для вызова метода. Это помогало писать программы с меньшим количеством строк кода в Java 7. Однако, синтаксис оставался достаточно сложным и громоздким.

Java 8 расширила возможности SAM, сделав шаг вперед. Как мы знаем, функциональный интерфейс содержит только один метод, следовательно, нам не нужно указывать название метода при передаче его в качестве аргумента. Именно это и позволяет нам lambda-выражения.

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

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

Как записать лямбда-выражение в Java?

В Java, лямбда-выражения имеют следующий синтаксис:


Здесь мы использовали новый оператор (->) — лямбда-оператор. Возможно, синтаксис кажется немного сложным. Давайте разберем пару примеров.

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


Мы можем записать его, используя лямбда, как:


Этот метод не имеет никаких параметров. Следовательно, левая часть выражения содержит пустые скобки. Правая сторона – тело лямбда-выражения, которое определяет его действие. В нашем случае, возвращается значение 3.1415.

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

В Java, тело лямбды может быть двух типов.

1. Однострочные


2. Блочные (многострочные)


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

Примечание: многострочные лямбда-выражения, всегда должны иметь оператор return, в отличии от однострочных.

Пример 3: лямбда-выражение

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

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

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


Результат выполнения:

  • Мы создали функциональный интерфейс MyInterface, который содержит один абстрактный метод getPiValue().
  • Внутри класса Main, мы объявили ссылку на MyInterface. Обратите внимание, что мы можем объявить ссылку на интерфейс, но не можем создать его объект.

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

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


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

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


Результат выполнения:

Параметризированный функциональный интерфейс

До этого момента, мы использовали функциональные интерфейсы, которые принимали только один тип значения. Например:


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

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


Результат выполнения:


В этом примере, мы создали параметризированный функциональный интерфейс GenericInterface, который содержит параметризированный метод func().

Затем, внутри класса Main:

  • GenericInterface<String> reverse – создает ссылку на интерфейс, который работает со String.
  • GenericInterface<Integer> factorial — создает ссылку на интерфейс, который работает с Integer.

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

В JDK8 добавлен новый пакет java.util.stream, который позволяет java-разработчикам выполнять такие операции, как поиск, фильтрация, сопоставление, объединение или манипулирование коллекциями, к примеру Lists.

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

Для этого мы можем использовать комбинацию Stream API и лямбда-выражений.

Пример 6: использование лямбд в Stream API


Результат выполнения:


В приведенном выше примере обратите внимание на это выражение:


Здесь мы используем такие методы, как filter(), map(), forEach() из Stream API, которые могут принимать лямбды в качестве параметра.

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

Это внутренний класс без имени, для которого создается только один объект. Анонимный внутренний класс может быть полезен при создании экземпляра объекта с некоторыми «дополнительными функциями», такими как методы перегрузки класса или интерфейса, без необходимости фактически создавать подкласс класса.

Анонимные внутренние классы полезны при написании классов реализации для интерфейсов слушателей в графическом программировании.

Анонимный внутренний класс в основном создается двумя способами:

  • Класс (может быть абстрактным или конкретным )
  • Интерфейс

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

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

// Java-программа для демонстрации
// нужен анонимный внутренний класс

public static void main(String[] args)

// Myclass - класс реализации интерфейса Age

MyClass obj = new MyClass();

// вызов метода getage (), реализованного в Myclass


// Myclass реализует методы Age Interface

class MyClass implements Age <

public void getAge()

System.out.print( "Age is " + x);

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

Лямбда-выражения в основном выражают экземпляры функциональных интерфейсов (интерфейс с единственным абстрактным методом называется функциональным интерфейсом. Примером является java.lang.Runnable). Лямбда-выражения реализуют единственную абстрактную функцию и, следовательно, реализуют функциональные интерфейсы

Лямбда-выражения добавлены в Java 8 и предоставляют следующие функциональные возможности.

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

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


// Пример функционального интерфейса (Интерфейс с
// один абстрактный метод

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

теперь можно сделать с помощью Lambdas:

и выглядит удивительно лаконично. Итак, мой вопрос: есть ли причина продолжать использовать эти классы в Java8 вместо Lambdas?

EDIT

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

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

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

обновление

еще одно различие между AICS и лямбда-выражениями заключается в том, что AICs вводит новую область. То есть имена разрешаются из суперклассов и интерфейсов AIC и могут затенять имена, которые происходят в лексически заключающей среде. Для лямбд, все имена разрешаются лексически.

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

так, нет, мы не можем просто игнорировать анонимные классы. И просто FYI, код sort() метод можно упростить, пропустив объявление типа для p1 и p2 :

вы также можете использовать метод ссылка здесь. Либо вы добавляете compareByFirstName() метод Person класса, и использовать:

или добавьте геттер для firstName , непосредственно получить Comparator С Comparator.comparing() способ:

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

при запуске приложения каждый файл класса должен быть загружен и проверен.

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

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

Для Лямбды эта инструкция используется для задержки перевода лямбда-выражения в байт-код до времени выполнения. (инструкция будет вызвана только в первый раз)

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

есть следующие различия:

1 синтаксис)

лямбда-выражения выглядят аккуратно по сравнению с анонимным внутренним классом (AIC)

2) Scope

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

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

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

3) Производительность

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

**Я понимаю, что это не совсем верно . Пожалуйста, см. Следующий вопрос. лямда против анонимная производительность внутреннего класса: снижение нагрузки на загрузчик классов?


Поддержка лямбда-выражений, реализованная в Java 8, стала одним из наиболее значимых нововведений за последнее время. Будучи упрощённой записью анонимных классов, лямбды позволяют писать более лаконичный код при работе со Stream или Optional. Лямбда-выражения часто используются как совместно со многими API стандартной библиотеки Java, так и со сторонними API, среди которых JavaFX, реактивные стримы и т.д.

Лямбды и функциональные интерфейсы

Пример простого функционального интерфейса:

Структура лямбда-выражения

Типы аргументов лямбда-выражения опциональны, так как они декларируются интерфейсом, но при использовании обобщений (дженериков) с extends/super может возникнуть необходимость в указании конкретных типов аргументов. При этом стоит отметить, что типы либо указываются для всех аргументов, либо не указываются вообще. Это же касается и использования var, введённой в Java 11. Всё это можно свести к такому правилу: все аргументы объявляются либо с типами, либо с var, либо без них.

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

Аналогичная ситуация и с телом лямбда-выражений: если оно состоит только из одной строки, то фигурные скобки, точку с запятой (;) и директиву return можно тоже опустить.

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

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

Допустим, нам нужна реализация CarFilter, описанного выше, которая проверяла бы, что автомобиль выпущен не раньше 2010 года. Если мы будем использовать анонимный класс, то создание объекта CarFilter будет выглядеть примерно следующим образом:

Но мы можем описать объект CarFilter при помощи лямбда-выражения:

Однако, эту запись можно сделать ещё меньше:

Согласитесь, что такая запись зачительно меньше и лаконичнее, чем использование анонимного класса.

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

Скорее всего, мы напишем что-то вроде:

В целом, если нам требуется всего один подобный метод, то этот код можно оставить без изменений и даже не задумываться об использовании лямбда-выражений. Но, допустим, у нас появляется задача реализовать ещё один метод, который бы выводил все автомобили, у которых кузов не PICKUP_TRUCK, или метод, который бы сохранял в БД все автомобили с мощностью двигателя более 150 л.с.

java.util.function.Predicate декларирует абстрактный метод test, который принимает объект и возвращает значение типа boolean в зависимости от соответствия переданного объекта требуемым критериям.

java.util.function.Consumer декларирует абстрактный метод accept, который принимает объект и выполняет над ним требуемые действия.

Метод printCars превратится во что-то похожее на следующий метод:

Или при помощи анонимных классов:

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

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

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

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

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

Язык программирования Java позволяет объявлять классы внутри другого класса. Такой класс называется вложенным классом:

Вложенные классы бывают статическими и нестатическими. Вложенные классы, объявленные с ключевым словом static , называются статическими вложенными классами (static nested classes). Вложенные классы, объявленные БЕЗ ключевого слова static, называются внутренними классами (inner classes).

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

Как члены класса вложенные классы могут быть объявлены с ключевым словом private , protected , public или без модификатора доступа (package-private).

Внешний класс (OuterClass) может быть только public или package-private!

Для чего использовать вложенные классы

Причины для использования вложенных классов в Java:

  • Логическая группировка классов, которые используются только в одном месте. Если класс используется только одним другим классом, то есть смысл вложить его в этот класс, чтобы обозначить их связь.
  • Увеличение инкапсуляции. Если класс B должен обращаться к членам класса A , которые в противном случае были бы объявлены private , то имеет смысл вложить класс B в класс A , тогда эти члены можно будет объявить private , но B сможет к ним обращаться. В дополнение B можно будет скрыть от внешнего мира.
  • Облегчение чтения и сопровождения кода. Маленькие классы можно вложить во внешние классы, ближе к месту использования.

Статические вложенные классы

Статические вложенные классы связаны со своим внешним классом так же, как методы и переменные.

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

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

К статическим вложенным классам обращаются через имя их внешнего класса:

Либо можно импортировать статический вложенный класс и обращаться к нему по имени:

Внутренние классы бывают:

  • Нестатическими членами класса.
  • Локальными классами.
  • Анонимными классами.

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

Внутренний класс, являющийся нестатическим членом класса

Внутренний класс будет нестатическим членом класса, если он объявлен прямо внутри тела внешнего класса:

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

Нестатические вложенные классы, являющиеся членами класса, могут быть объявлены с любым из модификаторов private , protected , public или без модификатора (package-private).

Локальные классы

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

Локальные классы не могу иметь никаких модификаторов доступа: ни private , ни protected , ни public .

Анонимные классы

Анонимные классы объявляются внутри выражения с ключевым словом new . Пример:

Выражение анонимного класса состоит из:

  • Операции new .
  • Имени интерфейса для реализации или родительского класса. В данном примере используется интерфейс MyInterface .
  • Скобки с аргументами для конструктора родительского класса. Анонимный класс не может объявить в своём теле новых конструкторов, так как у него нет имени.
  • Тело класса.

Анонимный класс никогда не может быть abstract (абстрактные классы будут рассмотрены позже).

Анонимный класс всегда неявно final .

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

Затенение переменных

Если имя переменной в какой-либо области имеет такое же имя, что и переменная во внешней области, то такая переменная затеняет (shadow) переменную из внешней области. Вы не можете обратиться к переменной из внешней области просто по имени. Пример ниже показывает, как нужно обращаться к затенённой переменной:

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

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

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

То будет ошибка компиляции, так как переменная x в этой области уже объявлена.

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

  • Объявления переменных.
  • Операции присвоения.
  • Операторы return .
  • Инициализации массивов.
  • Аргументы конструкторов или методов.
  • Тела лямбда-выражений.
  • условные операторы, ? : .
  • Выражения приведения типа.

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

Пакет java . util . function содержит большое количество стандартных интерфейсов, которые специально предназначены для использования с лямбда-выражениями. Хотя в примерах выше мы всегда создавали свой интерфейс, но в реальных приложениях рекомендуется использовать подходящий стандартный интерфейс, который можно поискать в документации. Все интерфейсы там обобщённые (статья про обобщение в Java будет написана позже), и могут использоваться с любым объектом.

Некоторые из функциональных интерфейсов пакета java . util . function :

Consumer <T> — содержит один метод с одним объектом в качестве параметра без результата метода.

Function < T , R > — содержит один метод с одним объектом в качестве параметра, возвращающий другой объект в качестве результата.

Predicate <T> — содержит один метод с объектом в качестве параметра, возвращающий результат boolean .

Supplier <T> — содержит один метод без параметров, возвращающий объект.

Ссылки на методы

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

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