Любой анонимный класс можно заменить на лямбду

Добавил пользователь Алексей Ф.
Обновлено: 20.09.2024

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

Если вы проект перевели на Java 8, то такие измения безопасные.

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

19) императивный vs декларативный подход

Это такой стиль программирования, при котором вы описываете, как добиться желаемого результата. Например я пишу:
— поставь сковородку на огонь;

— возьми два яйца (куриных);

— нанеси удар ножом по каждому;

— вылей содержимое на сковородку;

Почему пример из части про императивность на самом деле с примесью декларативности?

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

Императивный подход (как): Я вижу, что тот угловой столик свободен. Мы пойдём туда и сядем там.
Декларативный подход (что): Столик для двоих, пожалуйста.
Императивный подход означает то, как вы займёте место. Вы должны перечислить все шаги этого процесса.

Декларативный же подход заявляет, что вам нужен столик на двоих

20) Что такое default методы в интерфейсе и для чего они были введены?

Default-методы появились Java 8.

Default-метод — это метод, который реализуется прямо в интерфейсе, его помечают ключевым словом default.

default-методы упрощают рефакторинг — а именно, добавление новых методов.

реализующие интерфейс — реализовывать метод в этих классах. Это было неудобно. А в Java 8 (в классы ядра) захотели ввести новые методы в старые

интерфейсы. Так что ввели ключевое слово default и эти методы сделали default.

Например, в интерфейсе java.lang.Iterable появились новые default-методы forEach() и spliterator():

какой метод унаследует класс, реализующий два интерфейса, если оба из них содержат default-методы с одинаковыми именами.

Чтобы не было неопределенности (и чтобы скомпилировался код), мы обязаны переопределить в Kentavr метод sleep()

, причем можно просто вызвать в нем метод sleep() любого из интерфейсов — Man либо Animal, указав через точку и super, чей именно метод нужен:

21) К каким переменным есть доступ из лямбда-выражения?
Доступ к переменным внешней области действия из лямбда-выражения очень схож к доступу из анонимных объектов. Можно ссылаться на:

1) неизменяемые (effectively final - не обязательно помеченные как final) локальные переменные;

3) статические переменные.

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

effectively final локальные переменные

В в лямбда-выражениях стоит использовать внешние (относительно выражения) неизменяемые значения, а не внешние переменные,

значение и внутреннее состояние которых могут меняться. Под внешними неизменяемыми значениями, соответственно,

подразумеваются effectively final локальные переменные и поля примитивных типов, а также effectively final объекты,

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

22) Любой анонимный класс можно заменить на лямбду?

В Java 8 можно любой анонимный класс, который реализует один абстрактный метод заменить на лямбду выражение,

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

24) Отличие BinaryOperator от Function.

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

Функциональный интерфейс Function представляет функцию перехода от объекта типа T к объекту типа R.

25) Что такое ленивая инициализация стрима?

Отложенная (ленивая) инициализация (англ. . Lazy initialization) — приём в программировании,

когда некоторая ресурсоёмкая операция (создание объекта, вычисление значения)

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

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

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

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

Для таких методов, как reduced () и findFirst (),

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

26) Две терминальные операции в одном выражении?

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

27) Что такое терминальная операция?

но они не будут выполнены пока не будут добавлена терминальная операция.
Выше мы уже применяли одну из самых популярных операций — forEach(Consumer action).

В нее попадают все прошедшие через стрим объекты и обрабатываются в соответствие с тем алгоритмом, что будет указан в Consumer.

28) Что возвращают промежуточные операции над стримом?

Промежуточные (“intermediate”, ещё называют “lazy”) операции — обрабатывают поступающие элементы и возвращают стрим.

Промежуточных операторов в цепочке обработки элементов может быть много

А терминальные как раз инициируют всю цепочку преобразований и возвращают модифицированные данные.

29) Для чего нужны параллельные стримы?

Stream API предоставляет очень простой механизм для выполнения операций над потоком параллельно:

входной поток разбивается на части, если это возможно,

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

Кроме последовательных потоков Stream API поддерживает параллельные потоки.

Распараллеливание потоков позволяет задействовать несколько ядер процессора (если целевая машина многоядерная) и тем

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

что применение параллельных потоков на многоядерных машинах однозначно повысит производительность - не совсем корректно.

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

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

эти потоки будут вынуждены ждать друг друга

2) уследить за нежелательными эффектами от влияния нитей друг на друга становится ещё сложнее.

3) параллельность обработки вообще не гарантируется и зависит от источника данных.

А прирост производительности зависит от его способности корректно разделить набор данных на независимые блоки.
30) Что такое анонимный класс.

Анонимный класс (anonymous class) - это локальный класс без имени. Используется тогда, когда нужно переопределить метод класса или интерфейса.

31) Что такое функциональный интерфейс и для чего он нужен и зачем были добавлены?

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

Этот единственный метод определяет назначение интерфейса.

Это интерфейс, котрый определяет сторого один метод.

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

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

ФИ позволяют нам использовать лямбда выражения для создания экземпляра таких интерфейсоф

Интерфейс Runnable является одним из самых популярных, с одним методом run().

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

32) Какой аннотацией помечается функциональный интерфейс?

33) Сколько дефолтных методов и статических методов, сртатических полей в интерфейсе?

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

34) Где находятся функциональные интерфейсы?

В Java все стандартные функциональные интерфейсы лежат в пакете java.util.function

35) Перечислить основные семейства функ.интерфейсов?

Делятся на 7 семейств

1) Consumer - потребители, те кто принимают(объект типа Т, совершают некотрые действия) но не возвращают в замен. Его подвиды IntConsumer, LongConsumer, DoubleConsumer они есть

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

2) Supplir - поставщики, они не принимают ни какое значение, а просто возращают какоето значение типа Т.

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

4) Function - принимает аргумент типа Т и приводит его к объекту типа R, который и возвращается как результат, в общем случаее типы разные. Бывает принимают два параметра. могут стоять примитивы

5) Operator - это частный случай функции, когда на вход подается значение одного и того же типа.

Унарный оператор принимает один параметр а Бинарный два параметра отдельные интерфейсы над лонгами и даблами

6) BinaryOperator принимает в качестве параметра два объекта типа T, выполняет над ними бинарную операцию и возвращает ее результат также в виде объекта типа T.

7) UnaryOperator принимает в качестве параметра объект типа T, выполняет над ними операции и возвращает результат операций в виде объекта типа T.

36) Функциональные интерфейсы что они принимают и что возвращают?

1) Consumer - потребители, те кто принимают (объект типа Т, совершают некотрые действия) но не возвращают в замен. Его подвиды IntConsumer, LongConsumer, DoubleConsumer они есть

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

2) Supplir - поставщики, они не принимают ни какое значение, а просто возращают как оето значение типа Т.

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

4) Function - принимает аргумент типа Т и приводит его к объекту типа R, который и возвращается как результат, в общем случаее типы разные.

5) Operator - это частный случай функции, когда на вход подается значение одного и того же типа. Унарный оператор принимает один параметр а Бинарный два параметра

отдельные интерфейсы над лонгами и даблами

6) BinaryOperator принимает в качестве параметра два объекта типа T, выполняет над ними бинарную операцию и возвращает ее результат также в виде объекта типа T.

7) UnaryOperator принимает в качестве параметра объект типа T, выполняет над ними операции и возвращает результат операций в виде объекта типа T.

37) Какие есть способы инстацировать функциональные интерфейсы?

Функциональные интерфейсы можно интанцировать тремя способами

1) можно завести именнованный или анонимный класс, но это громоздко

2) можно использовать лямбда выражение. Обявляем имена параметра и тело метода sqware = x-> ; обязательно точка с запятой.

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

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

1) к параметрам лямбды, а также свободно объявлять и использовать любые переменные х -> x * x;

2) к полям(переменным) того класса в нутри котого объявлена лямбда, можно как читать, так и писать. IntSupplier sequense = () -> counter++;

3) к переменным которые объявлены внутри метода где объявлена лямбда. Но есть ограничения переменные должны быть эффективно финальные

т.е. значение им должно присвоено ровно один раз до объявления лямбды, после чего оно менятся уже не может.(Типа мы написали final)int bonus = 10; IntUnaryOperator bonusAdder = (x) -> x + bonus;

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

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

Введение

В этой статье, с помощью примеров, мы изучим 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 reverse – создает ссылку на интерфейс, который работает со String.
  • GenericInterface factorial — создает ссылку на интерфейс, который работает с Integer.

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

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

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

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

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


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


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


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

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

image

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

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

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

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

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

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

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

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

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

  • Анонимный класс генерируется компилятором Java.
  • Класс для реализации лямбда-выражения создается JVM во время выполнения.

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

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

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

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

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

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

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

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

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

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

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

  • Используется блок try-catch.
  • Исключение выбрасывается ещё раз.
  • Грязное использование стирания типов в Java.

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

  • Проверяемые исключения распознаются только на уровне Java компилятора.
  • Секция throws — это всего лишь метаданные для метода без семантического значения на уровне JVM.
  • Проверяемые и обычные исключения неразличимы на уровне байткода в JVM.

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

Этот код не скомпилируется, потому что у метода Callable.call объявлены проверяемые исключения в секции throws . Но мы можем убрать эту секцию, используя динамически сконструированное лямбда-выражение.

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

Второй шаг — создать реализацию этого интерфейса, используя LambdaMetafactory и делегировать вызов метода SilentInvoker.invoke методу Callable.call . Как было сказано ранее, секция throws игнорируется на уровне байткода, таким образом, метод SilentInvoker.invoke сможет вызвать метод Callable.call без объявления исключений:

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

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

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

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

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

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

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

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

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

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

Getter, объявленный в JavaBean — это метод с именем getXXX без параметров и возвращаемым типом данных, отличным от void . Setter, объявленный в JavaBean — метод с именем setXXX , с одним параметром и возвращающий void . Эти две нотации могут быть представленв как функциональные интерфейсы:

  • Getter может быть представлен классом Function, в котором аргумент — значение this .
  • Setter может быть представлен классом BiConsumer, в котором первый аргумент — this , а второй — значение, которое передается в Setter.

Теперь мы создадим два метода, которые смогут преобразовать любой getter или setter в эти
функциональные интерфейсы. И неважно, что оба интерфейса — generics. После стирания типов
реальный тип данных будет Object . Автоматическое приведение возвращаемого типа и аргументов может быть сделано при помощи LambdaMetafactory . В дополнение, библиотека Guava поможет с кэшированием лямбда-выражений для одинаковых getters и setters.

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

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

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

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

Информация о методе, полученная из экземпляра класса Method с использованием Java Reflection API может быть легко преобразована в MethodHandle . Примите во внимание, что у методов экземпляров класса, всегда есть скрытый первый аргумент, используемый для передачи this в этот метод. У статических методов такого параметра нет. Например, реальная сигнатура метода Integer.intValue() выглядит как int intValue(Integer this) . Эта хитрость используется в нашей имплементации функциональных оберток для getters и setters.

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

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

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

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

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

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

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

Как вы знаете, лямбда-выражение можно сконструировать динамически, используя LambdaMetaFactory . Чтобы это сделать, нужно определить обработчик — класс MethodHandle , который указывает на реализацию единственного метода, который определен в функциональном интерфейсе. Давайте взглянем на этот простой пример:

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

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

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

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

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

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

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

  • REF_invokeInterface: может быть создан при помощи Lookup.findVirtual для методов интерфейсов
  • REF_invokeVirtual: может быть создан с помощью Lookup.findVirtual для виртуальных методов класса
  • REF_invokeStatic: создается при помощи Lookup.findStatic для статических методов
  • REF_newInvokeSpecial: может быть создан при помощи Lookup.findConstructor для конструкторов
  • REF_invokeSpecial: может быть создан с помощью Lookup.findSpecial
    для приватных методов и раннего связывания с виртуальными методами класса

Остальные типы обработчиков вызовут ошибку LambdaConversionException .

Generic исключения

Этот баг связан с компилятором Java и возможностью объявлять generic исключения в секции throws . Следующий пример кода демонстрирует это поведение:

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

Из этого следует:

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

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

Можно сконструировать generic объект с несколькими ограничениями типов, используя знак & : .
Такой способ определения generic параметров редко используется, но определенным образом влияет на лямбда-выражения в Java из-за некоторых ограничений:

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

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

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

  • MutableInteger наследуется от Number .
  • MutableInteger реализует IntSupplier .

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

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

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

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

Первый раунд. Пространство на экране.

Лямбда-кун: хе. Хе-хе-хе. Нет, это несерьёзно. Ну как может сравниться вот это убожество:

С вот этой красотой:

Анон-сан: ну-ну, молодой человек, незачем так выражаться. В наши дни никого не волнует, что там в файле на самом деле. Достаточно взять хорошую IDE и разница уже практически незаметна.

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

Вот как выглядит анонимный класс:

Лямбда

А вот лямбда:

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

Второй раунд. Пространство на диске.

Лямбда-кун: кх-хм… Нет, это, конечно, нечестно. Ну ладно. Но на диске-то я занимаю меньше. Возьмём простой класс:

А с тобой будет вот что:

52 байта против 126! Каково, а?

Анон-сан: ну с байтами исходников я соглашусь, хотя кого они волнуют. А если скомпилировать?

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

Анон-сан: не спешите, молодой человек, давайте проверим. Запускаем javac Test.java для обоих версий. Что мы видим? Вариант с анонимным классом генерирует Test.class (308 байт) и Test$1.class (377 байт), всего 685 байт, а вариант с лямбдой генерирует только Test.class , зато он весит 783 байта. Почти сто байтов оверхед — не дороговато ли за синтаксический сахар?

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

Анон-сан: давайте попробуем: javac -g Test.java . Лямбда — 838 байт, анонимный класс — 825 байт. Так разница меньше, но всё же хвалёной легковесности не видно. Вы забываете, что на каждый вызов лямбды создаётся развесистая запись в Bootsrtap methods, которая анонимным классам не нужна.

Лямбда-кун: стой, стой. Я всё понял. Это не сама запись развесистая, а константы, которые попадают в пул констант. Всякие штуки вроде java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; . Да, они длинные, но переиспользуются, если лямбд больше одной. Ну-ка, добавим вторую:

Лямбда-кун: вооот, уже я выигрываю! С отладочной информацией 957 байт, а если заменить на анонимные классы, будет ажно 1351 байт в трёх файлах. Даже без отладочной информации я выигрываю. А ведь могут и другие константы эффективно переиспользоваться! Любые поля, методы, классы, используемые внутри лямбд. Если они используются в нескольких лямбдах или в лямбде и вокруг неё, то схлопнутся в одну константу. А с анонимными классами в каждом будет копия. То-то же!

Третий раунд. Классы в рантайме.

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

Лямбда-кун: а вот и не те же! Я же легковеснее! Там наверняка сгенерируется маленький компактный классик, который не содержит всякой ненужной ерунды. Да и как ты это проверишь? Оно ж всё в рантайме в памяти!

Анон-сан: а вот этого вам стыдно не знать. Должны же вас как-то отлаживать разработчики. Есть недокументированное системное свойство jdk.internal.lambda.dumpProxyClasses , с помощью которого можно указать, в какой каталог скидывать сгенерированные рантайм-представления лямбд. Запускаем приложение с -Djdk.internal.lambda.dumpProxyClasses=. и всё видим.

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

Анон-сан: нет никакой разницы. Даже наоборот, разница не в вашу пользу. Анонимный класс существует всегда на диске, но он не будет загружен в память, пока не используется. Рантайм-представление лямбды, конечно, сгенерировано не будет, однако её тело в виде приватного синтетического метода загружается фактически вместе с классом, в котором она объявлена. Даже если тело ни разу не используется, оно память отъест. Впрочем, к этому вопросу вернёмся позднее. Посмотрим сперва, что происходит, если лямбда используется. Для этого нам потребуется немного модифицировать программу:

Компилируем (хорошо, пусть с отладочной информацией), запускаем java -Djdk.internal.lambda.dumpProxyClasses=. Test и видим: лямбда создала класс Test$$Lambda$1.class , который весит 308 байт. Это помимо основного класса Test.class , который весит 1004 байта. Заменяем лямбду на аналогичный анонимный класс, имеем 508+399 байт в двух классах сразу, но в рантайме ничего не создаётся. Много вы всё-таки кушаете, молодой человек, на 405 байт больше меня.

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

Анон-сан: да хоть десять. Дописываем static Runnable r1 = () -> <>; и так далее. Получается 11 классов, с лямбдами 5174 байта, а с анонимными — 5059 байт. Неспеша догоняю я вас, конечно, но, согласитесь, уж 10 лямбд не в каждом классе есть. Где-то после 14-го анонимного класса только вы начинаете кушать меньше.

Лямбда-кун: так-так. А давай-ка эти все лямбды поместим прямо в метод main() . Согласись, нечасто они в статических полях лежат, нэ?

Анон-сан: хм, а в чём разница?

Лямбда-кун: а скомпилируй, и увидишь. У меня-то как раз разницы нет, сгенерированные в рантайме классы весят столько же. А у тебя каждый байт на 40 потолстел. Теперь на десяти лямбдах я кушаю меньше (5290 байт против 4995). Уже даже на шести я тебя опережаю!

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

Лямбда-кун: эта запись добавляется даже при полностью отключенной отладочной информации ( javac -g:none ). Этот атрибут обязан быть по спецификации вне зависимости от отладки. А моё рантайм-представление формально не является анонимным классом, и ему этот атрибут не нужен. Тебе ещё повезло, что имя метода main такое короткое. Если анонимные классы в методе с длинным названием, каждый будет отъедать дополнительно пропорционально его длине!

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

Ну и для анонимных классов заменим на new IntSupplier() > . Как вы думаете, сколько теперь потребуется лямбд, чтобы победить анонимные классы?

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

Анон-сан: такой, да не такой. Пробуем. Одна лямбда: 1493 байта, один анонимный класс: 1006 байт. Десять лямбд: 6803 байта, десять анонимных классов: 6039 байт. Двадцать лямбд: 12743 байта, двадцать анонимных классов: 11669 байт. Разрыв постоянно увеличивается! Тут хоть тысяча лямбд, а вам меня не догнать.

Лямбда-кун: ээ… Так. Ну-ка, декомпилируем. Это ещё что за ерунда? Какой-то фабричный метод? Глупость какая-то. Помимо конструктора мне ещё зачем-то добавляют метод вида static IntSupplier get$Lambda(int i) < return new Test$1(i);>. Бред какой-то, зачем?

Анон-сан: не бред, а производительность. Когда-то в незапамятные времена Walrus исправил скорость инстанциирования лямбд в интерпретаторе (JDK-8023984). Фабричный метод оказался быстрее, чем конструктор. Заметьте, молодой человек, со мной таких странных проблем не возникает, у меня всё быстро и так.

Лямбда-кун: вот же глупость-то! Нет чтобы допилить свои методхэндлы до ума, они костыли лепят… Интересно, может уже с тех пор допилили и этот метод не нужен стал.

Анон-сан: как знать, как знать.

Лямбда-кун: однако мой ход! Принимаю твоё условие и захват переменной, но давай-ка не IntSupplier , а Supplier :

Ну а лямбда останется как раньше: Supplier s = () -> i .

Лямбда-кун: выходит, что нет. Лямбде позволительно, чтобы для неё это не работало!

Анон-сан: однако хотя эта строчка отъест место, не верится мне, что сильно много против вашего фабричного метода.

Лямбда-кун: а ты не верь, а проверь. Теперь всего три лямбды кушают меньше, чем три анонимных класса (2963 против 3034 байта) и с каждой новой строчкой ты всё больше проигрываешь! Каждый анонимный класс кушает на 270 байт больше соответствующей лямбды. И это с учётом того, что у меня лишний фабричный метод!

Анон-сан: не может быть. Что же там ещё напихал-то компилятор? Ааа, как же я мог забыть. Бридж-метод. Так как в коде у нас Integer get() <> , а в интерфейсе после erasure — Object get() , нужен ещё мостик, который при вызове интерфейса перенаправит к Integer get() . А вам что ли мостик не нужен?

Лямбда-кун: нет, и мостик нам не нужен. Точнее наоборот, он нам нужен всегда, Object get() — это мостик, а реальная реализация в основном классе в синтетическом методе вида lambda$main$1 . Но мостик всегда один, в случае с генериками второй мостик не нужен. А вот тебе потребовался и тут стало понятно, что мы всё-таки на самом деле легковеснее!

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

Анонимный класс можно заменить лямбдой?

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

2 ответа

Анонимный новый можно заменить на лямбда?

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

Поскольку ActionListener имеет только один метод actionPerformed(ActionEvent e) , вы можете написать его, используя лямбда-выражение

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

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

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

Ваш код можно изменить на этот:

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

Лямбда действует как реализация метода actionPerformed , а (event) является аргументом этого метода, а -> < . >является телом этого метода.

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