1. Вступление


В этом туториале мы рассмотрим важный API, представленный в Java 7 и расширенный в новых версиях, java.lang.invoke.MethodHandles.



Мы узнаем, что такое method handles, как их создавать и использовать.


2. Что такое Method Handles?


В документации API method handle имеет такое определение:


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

Другими словами, method handles — это низкоуровневый механизм для поиска, адаптации и вызова методов. Объекты method handles неизменяемые и не имеют отображаемого состояния.


Для создания и использования MethodHandle нужно выполнить 4 действия:


  1. Создать описатель для поиска — lookup
  2. Объявить тип метода
  3. Выполнить поиск method handle
  4. Вызвать method handle

2.1. Method Handles vs Reflection


Method handles были представлены для функционирования наряду с java.lang.reflect API, т.к. они созданы для разных целей и отличаются по своим характеристикам.


С точки зрения производительности, MethodHandles API может оказаться намного быстрее Reflection API, поскольку проверки доступа выполняются во время создания, а не исполнения. При наличии security manager’а это различие увеличивается, т.к. поиск классов и получение их элементов подвергаются дополнительным проверкам.


Однако, производительность — не единственный показатель оптимальности задачи, нужно учитывать, что MethodHandles API сложнее в использовании из-за недостатка таких механизмов, как получение методов класса, проверка маркеров доступа и др.


Несмотря на это, MethodHandles API дает возможность каррировать методы, менять тип и порядок параметров.


Теперь, зная определение и предназначение MethodHandles API, можем работать с ними. Начнем с поиска методов.


3. Создание Lookup


Первое, что нужно сделать, когда мы хотим создать method handle, — это получить lookup, объект-фабрику, отвечающий за создание method handles для методов, конструкторов и полей, видимых для класса lookup.


С помощью MethodHandles API можно создать lookup-объект с разными режимами доступа.


Создадим lookup, предоставляющий доступ к public-методам:


MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();

Однако, если нам нужен доступ к методам private и protected, вместо этого мы можем использовать метод lookup():


MethodHandles.Lookup lookup = MethodHandles.lookup();

4. Создание MethodType


Для создания MethodHandle lookup-объекту необходимо задать тип, и это можно сделать с помощью класса MethodType.


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


Структура MethodType проста, она формируется возвращаемым типом вместе с соответствующим числом типов параметра, которые должны полностью соотноситься между method handle и вызывающим кодом.


Так же, как и MethodHandle, все экземпляры MethodType неизменяемы.


Посмотрим, как определить MethodType, задающий класс java.util.List в качестве типа возвращаемого значения и массив Object в качестве типа ввода данных:


MethodType mt = MethodType.methodType(List.class, Object[].class);

В случае, если метод возвращает простой или void тип значения, мы используем класс, представляющий эти типы (void.class, int.class …).


Определим MethodType, который возвращает значение int и принимает Object:


MethodType mt = MethodType.methodType(int.class, Object.class);

Можно приступать к созданию MethodHandle.


5. Поиск MethodHandle


После того, как мы задали тип метода, для создания MethodHandle нужно найти его с помощью объекта lookup или publicLookup, который также выдает исходный класс и имя метода.


Lookup предоставляет набор методов, позволяющий находить method handle оптимальным способом с учетом области видимости метода. Рассмотрим основные подходы, начиная с простейших.


5.1. Method Handle для методов


С помощью метода findVirtual() можно создать MethodHandle для метода экземпляра. Создадим его на основе метода concat() класса String:


MethodType mt = MethodType.methodType(String.class, String.class);
MethodHandle concatMH = publicLookup.findVirtual(String.class, "concat", mt);

5.2. Method Handle для статических методов


Для получения доступа к статическому методу можно использовать метод findStatic():


MethodType mt = MethodType.methodType(List.class, Object[].class);
MethodHandle asListMH = publicLookup.findStatic(Arrays.class, "asList", mt);

В данном случае мы создали method handle метода, преобразующего массив типа Object в список List.


5.3. Method Handle для конструкторов


Получить доступ к конструктору можно с помощью метода findConstructor().


Создадим method handle с поведением, как у конструктора класса Integer с параметром String:


MethodType mt = MethodType.methodType(void.class, String.class);
MethodHandle newIntegerMH = publicLookup.findConstructor(Integer.class, mt);

5.4. Method Handle для полей


С помощью method handle можно также получить доступ к полям.


Начнем с определения класса Book:


public class Book {
    String id;
    String title;

    // constructor
}

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


MethodHandle getTitleMH = lookup.findGetter(Book.class, "title", String.class);

Более подробную информацию об управлении переменными/полями ищите в статье Java 9 Variable Handles Demystified, где мы рассказываем о java.lang.invoke.VarHandle API, введенном в Java 9.


5.5. Method Handle для Private методов


Создать method handle для метода типа private можно с помощью java.lang.reflect API.
Начнем с того, что создадим private метод для класса Book:


private String formatBook() {
    return id + " > " + title;
}

Теперь мы можем создать method handle с поведением метода formatBook():


Method formatBookMethod = Book.class.getDeclaredMethod("formatBook");
formatBookMethod.setAccessible(true);

MethodHandle formatBookMH = lookup.unreflect(formatBookMethod);

6. Вызов Method Handle


Как только мы создали наш method handle, приступаем к следующему шагу. Класс MethodHandle дает нам 3 разных способа вызова method handle: invoke(), invokeWithArugments() и invokeExact().


Начнем со способа invoke.


6.1. Вызов Method Handle


При использовании метода invoke() количество аргументов (arity) фиксируется, но при этом возможно выполнение приведения типов и упаковка/распаковка аргументов и типов возвращаемого значения.


Теперь посмотрим, как можно использовать invoke() с упакованным аргументом:


MethodType mt = MethodType.methodType(String.class, char.class, char.class);
MethodHandle replaceMH = publicLookup.findVirtual(String.class, "replace", mt);

String output = (String) replaceMH.invoke("jovo", Character.valueOf('o'), 'a');

assertEquals("java", output);

В данном случае replaceMH требуются аргументы char, но метод invoke() распаковывает аргумент Character до его исполнения.


6.2. Вызов с аргументами


Вызов method handle с помощью метода invokeWithArguments имеет меньше всего ограничений.


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


На практике мы можем создать список Integer, имея массив значений int неизвестной длины:


MethodType mt = MethodType.methodType(List.class, Object[].class);
MethodHandle asList = publicLookup.findStatic(Arrays.class, "asList", mt);

List<Integer> list = (List<Integer>) asList.invokeWithArguments(1, 2);

assertThat(Arrays.asList(1,2), is(list));

6.3. Вызов Exact


Если нам необходимо, чтобы method handle выполнялся более ограниченно (по набору аргументов и их типу), мы используем метод invokeExact().


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


Посмотрим, как можно выполнить сложение двух значений int с помощью method handle:


MethodType mt = MethodType.methodType(int.class, int.class, int.class);
MethodHandle sumMH = lookup.findStatic(Integer.class, "sum", mt);

int sum = (int) sumMH.invokeExact(1, 11);

assertEquals(12, sum);

В данном случае, если передать в метод invokeExact число, не являющееся int, при вызове мы получим WrongMethodTypeException.


7. Работа с массивами


MethodHandles могут работать не только с полями и объектами, но и с массивами. При помощи asSpreader() API можно создать method handle, поддерживающий массивы в качестве позиционных аргументов.


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


Посмотрим, как получить method handle, чтобы проверить, являются ли аргументы массива одинаковыми строками:


MethodType mt = MethodType.methodType(boolean.class, Object.class);
MethodHandle equals = publicLookup.findVirtual(String.class, "equals", mt);

MethodHandle methodHandle = equals.asSpreader(Object[].class, 2);

assertTrue((boolean) methodHandle.invoke(new Object[] { "java", "java" }));

8. Уточнение Method Handle


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


Например, в Java 9 этот трюк используется для оптимизации конкатенации строк.


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


MethodType mt = MethodType.methodType(String.class, String.class);
MethodHandle concatMH = publicLookup.findVirtual(String.class, "concat", mt);

MethodHandle bindedConcatMH = concatMH.bindTo("Hello ");

assertEquals("Hello World!", bindedConcatMH.invoke("World!"));

9. Обновления Java 9


В Java 9 было внесено несколько изменений в MethodHandles API, чтобы упростить их использование.


Обновления касаются 3 основных аспектов:


  • Функции lookup – допускают поиск из разных контекстов и поддерживают неабстрактные методы в интерфейсах.
  • Операции с аргументами – улучшение функционала свертывания, сбора и распределения аргументов.
  • Дополнительные комбинации – добавление операций цикла (loop, whileLoop, doWhileLoop, ...) и улучшенное управление исключениями с помощью tryFinally.

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


  • Улучшенная оптимизация JVM компилятора
  • Снижение инстанционирования
  • Конкретизирование использования MethodHandles API

Более подробный список изменений доступен в MethodHandles API Javadoc.


10. Заключение


В этой статье мы познакомились с MethodHandles API, а также узнали, что из себя представляют Method Handles и как их использовать.


Мы также описали, как он связан с Reflection API. Так как вызов method handles это довольно низкоуровневая операция, их использование оправдано только в том случае, если они в точности подходят под ваши задачи.


Как обычно, весь исходный код для статьи доступен на Github.

Комментарии (9)


  1. itblogger
    04.12.2018 12:23

    … и только совсем непонятно зачем они нужны… В статье об этом ни слова. Или я плохо читал?


    1. jreznot Автор
      04.12.2018 12:50

      Мы используем для улучшения производительности. Создавая Method handle единожды, избегаем постоянных проверок во время вызова, которые есть в java.lang.reflect.Method.


      1. patex
        05.12.2018 11:16

        Насколько я помню в 8+ java java.lang.reflect.Method (если отключить проверки) немного быстрее methodHandle, к тому же можно пойти дальше и сделать из methodHandle лямбду используя LambdaMetafactory.metafactory что даст еще больше удобства и производительности


        1. jreznot Автор
          05.12.2018 11:20

          Да, мы LambdaMetafactory.metafactory тоже юзаем, генерируем Consumer<T> на лету — UiControllerReflectionInspector


  1. Greben86
    04.12.2018 13:04
    +1

    При использовании логирования очень удобно создавать логер следующим образом:
    Logger LOGGER = Logger.getLogger(MethodHandles.lookup().lookupClass().getName());
    Это избавляет от ошибок при копировании, так как эта строка как правило кочует из класса в класс в неизменном виде


    1. alatushkin
      04.12.2018 15:20
      -1

      Почему вы не хотите использовать lombok?


      1. Greben86
        04.12.2018 16:13

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


      1. jreznot Автор
        04.12.2018 16:54

        Ну вообще, в Lombok очень плохо, что нужен плагин для IDE.


        1. yaremchuk
          05.12.2018 11:20

          А что в этом плохого? Ведь плагины для idea и netbeans есть? Да и сборкой всеравно обычно занимаются maven или gradle, с которыми lombok дружит в первую очередь.