От переводчика: 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, но есть следующие отличия: Реализация metafactory зависит от вендора JVM и от версии Конечно же, инструкция invokedynamic используется не только для лямбда-выражений в Java. В основном, она применяется при выполнении динамических языков в среде JVM. Движок Nashorn для исполнения JavaScript, который встроен в Java, интенсивно использует эту инструкцию. Далее мы сфокусируемся на классе LambdaMetafactory и его возможностях. Следующий
раздел этой статьи исходит из предположения, что вы отлично понимаете как работают методы metafactory и что такое MethodHandle  В этом разделе мы покажем, как строить динамические конструкции из лямбд для использования в ежедневных задачах.  Не секрет, что все функциональные интерфейсы, которые есть в Java, не поддерживают проверяемые исключения. Преимущества проверяемых исключений перед обычными — это очень давний (и до сих пор горячий) спор.  А что, если вам нужно использовать код с проверяемыми исключениями внутри лямбда-выражений в сочетании с Java Streams? Например, нужно преобразовать список строк в список URL как здесь: В конструкторе URL(String) объявлено проверяемое исключение, таким образом, он не может быть использован напрямую в виде ссылки на метод в классе Functiion. Вы скажете: "Нет, возможно, если использовать вот такую хитрость": Это грязный хак. И вот почему: Проблема может быть решена более "легальным" способом, с использованием знания следующих фактов: Решение — обернуть метод 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.  Этот раздел будет полезен тем, кто пишет сериализацию/десериализацию для различных форматов данных, таких как JSON, Thrift и т.д. Более того, он может быть довольно полезен, если ваш код сильно полагается на рефлексию для Getters и Setters в JavaBeans.  Getter, объявленный в JavaBean — это метод с именем getXXX без параметров и возвращаемым типом данных, отличным от void. Setter, объявленный в JavaBean — метод с именем setXXX, с одним параметром и возвращающий void. Эти две нотации могут быть представленв как функциональные интерфейсы: Теперь мы создадим два метода, которые смогут преобразовать любой 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 и работает очень медленно.  Как было показано ранее, не все обработчики методов могут быть использованы для создания лямбда-выражений во время выполнения кода.  Только несколько типов обработчиков методов могут быть использованы для динамического создания лямбда-выражений Вот они: Остальные типы обработчиков вызовут ошибку LambdaConversionException. Этот баг связан с компилятором Java и возможностью объявлять generic исключения в секции throws. Следующий пример кода демонстрирует это поведение:  Этот код должен скомпилироваться, потому что конструктор класса URL выбрасывает MalformedURLException. Но он не компилируется. Выдается следующее сообщение об ошибке: Но, если мы заменим лямбда-выражение анонимным классом, то код скомпилируется: Из этого следует: Вывод типов для generic исключений не работет корректно в сочетании с лямбда-выражениями Можно сконструировать generic объект с несколькими ограничениями типов, используя знак &: <T extends A & B & C & ... Z>.
Такой способ определения generic параметров редко используется, но определенным образом влияет на лямбда-выражения в Java из-за некоторых ограничений: Второе ограничение приводит к разному поведению кода во время компиляции и во время выполнения, когда происходит связываение с лямбда-выражения. Эту разницу можно продемонстрировать, используя следующий код: Этот код абсолютно корректный и успешно компилируется. Класс MutableInteger удовлетворяет ограничениям обобщенного типа T: Но код упадет с исключением во время выполнения: Так получается, потому что конвейер JavaStream захватывает только чистый тип, который, в нашем случае — класс Number и он не реализует интерфейс IntSupplier. Эту проблему можно исправить явным объявлением типа параметра в отдельном методе, используемом в качестве ссылки на метод: Этот пример демонстрирует некорректный вывод типов в компиляторе и среде исполнения. Обработка нескольких ограничений типов generic параметров в сочетании с использованием лямбда-выражений во время компиляции и выполнения — неконсистентна