1. Вступление
В этом туториале мы рассмотрим важный API, представленный в Java 7 и расширенный в новых версиях, java.lang.invoke.MethodHandles.
Мы узнаем, что такое method handles, как их создавать и использовать.
2. Что такое Method Handles?
В документации API method handle имеет такое определение:
Method handle — это типизированная, исполняемая ссылка на базовый метод, конструктор, поле или другую низкоуровневую операцию с дополнительными трансформациями аргументов или возвращаемых значений.
Другими словами, method handles — это низкоуровневый механизм для поиска, адаптации и вызова методов. Объекты method handles неизменяемые и не имеют отображаемого состояния.
Для создания и использования MethodHandle
нужно выполнить 4 действия:
- Создать описатель для поиска — lookup
- Объявить тип метода
- Выполнить поиск method handle
- Вызвать 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.