[Перевод] Что такое Method Handles в Java

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

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


j1-za-b6yefpc6ivulb-jawsk4a.png

Мы узнаем, что такое 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 list = (List) 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.

© Habrahabr.ru