1. Введение
В этой статье мы собираемся изучить важный API, представленный в Java 7 и улучшенный в следующих версиях, java.lang.invoke.MethodHandles
. ``
В частности, мы узнаем, что такое дескрипторы методов, как их создавать и как их использовать.
2. Что такое дескрипторы методов?
Что касается его определения, как указано в документации API:
Дескриптор метода — это типизированная, непосредственно исполняемая ссылка на базовый метод, конструктор, поле или аналогичную низкоуровневую операцию с необязательными преобразованиями аргументов или возвращаемых значений.
Проще говоря, дескрипторы методов — это низкоуровневый механизм поиска, адаптации и вызова методов .
Дескрипторы методов неизменяемы и не имеют видимого состояния.
Для создания и использования MethodHandle требуется
4 шага:
- Создание поиска
- Создание типа метода
- Поиск дескриптора метода
- Вызов дескриптора метода
2.1. Дескрипторы методов против отражения
Дескрипторы методов были введены для того, чтобы работать вместе с существующим API java.lang.reflect
, поскольку они служат разным целям и имеют разные характеристики.
С точки зрения производительности API-интерфейс MethodHandles
может быть намного быстрее, чем API-интерфейс Reflection, поскольку проверки доступа выполняются во время создания, а не во время выполнения . Эта разница усиливается, если присутствует менеджер безопасности, поскольку поиск членов и классов подлежит дополнительным проверкам.
Однако, учитывая, что производительность — не единственная мера пригодности для задачи, мы также должны учитывать, что API-интерфейс MethodHandles
сложнее использовать из-за отсутствия таких механизмов, как перечисление классов-членов, проверка флагов доступности и т. д.
Тем не менее, API MethodHandles
предлагает возможность каррирования методов, изменения типов параметров и их порядка.
Имея четкое определение и цели API MethodHandles
, теперь мы можем начать с ними работать, начиная с поиска.
3. Создание поиска
Первое, что нужно сделать, когда мы хотим создать дескриптор метода, — это получить поиск, фабричный объект, который отвечает за создание дескрипторов методов для методов, конструкторов и полей, видимых для класса поиска.
С помощью API MethodHandles
можно создать объект поиска с различными режимами доступа.
Давайте создадим справочник, предоставляющий доступ к общедоступным
методам:
MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();
Однако, если мы хотим иметь доступ также к закрытым
и защищенным
методам, мы можем вместо этого использовать метод lookup()
:
MethodHandles.Lookup lookup = MethodHandles.lookup();
4. Создание типа метода
Чтобы иметь возможность создать MethodHandle
, объекту поиска требуется определение его типа, и это достигается с помощью класса MethodType
.
В частности, MethodType представляет
аргументы и возвращаемый тип, принятые и возвращенные дескриптором метода или переданные и ожидаемые вызывающим дескриптором метода .
Структура MethodType
проста и формируется типом возвращаемого значения вместе с соответствующим количеством типов параметров, которые должны быть правильно согласованы между дескриптором метода и всеми его вызывающими объектами.
Так же, как и в случае с MethodHandle
, даже экземпляры MethodType
являются неизменяемыми.
Давайте посмотрим, как можно определить MethodType
, указывающий класс java.util.List
в качестве типа возвращаемого значения и массив объектов
в качестве типа ввода:
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,
мы должны найти его с помощью объекта поиска
или объекта publicLookup
, указав также исходный класс и имя метода.
В частности, фабрика поиска предоставляет набор методов , которые позволяют нам найти дескриптор метода надлежащим образом с учетом области действия нашего метода. Начав с самого простого сценария, давайте рассмотрим основные.
5.1. Дескриптор метода для методов
Использование метода findVirtual()
позволяет нам создать MethodHandle для метода объекта. Давайте создадим его на основе метода concat() класса
String
:
MethodType mt = MethodType.methodType(String.class, String.class);
MethodHandle concatMH = publicLookup.findVirtual(String.class, "concat", mt);
5.2. Дескриптор метода для статических методов
Когда мы хотим получить доступ к статическому методу, мы можем вместо этого использовать метод findStatic()
:
MethodType mt = MethodType.methodType(List.class, Object[].class);
MethodHandle asListMH = publicLookup.findStatic(Arrays.class, "asList", mt);
В этом случае мы создали дескриптор метода, который преобразует массив объектов
в их список
.
5.3. Дескриптор метода для конструкторов
Получить доступ к конструктору можно с помощью метода findConstructor()
.
Давайте создадим дескрипторы метода, который ведет себя как конструктор класса Integer
, принимая атрибут String :
MethodType mt = MethodType.methodType(void.class, String.class);
MethodHandle newIntegerMH = publicLookup.findConstructor(Integer.class, mt);
5.4. Дескриптор метода для полей
Используя дескриптор метода, можно также получить доступ к полям.
Приступим к определению класса Book :
public class Book {
String id;
String title;
// constructor
}
Имея в качестве предварительного условия видимость прямого доступа между дескриптором метода и объявленным свойством, мы можем создать дескриптор метода, который ведет себя как геттер:
MethodHandle getTitleMH = lookup.findGetter(Book.class, "title", String.class);
Для получения дополнительной информации об обработке переменных/полей, взгляните на Java 9 Variable Handles Demystified , где мы обсуждаем API java.lang.invoke.VarHandle , добавленный в Java 9.
5.5. Дескриптор метода для частных методов
Создать дескриптор метода для частного метода можно с помощью API java.lang.reflect .
Давайте начнем добавлять закрытый
метод в класс Book :
private String formatBook() {
return id + " > " + title;
}
Теперь мы можем создать дескриптор метода, который ведет себя точно так же, как метод formatBook()
:
Method formatBookMethod = Book.class.getDeclaredMethod("formatBook");
formatBookMethod.setAccessible(true);
MethodHandle formatBookMH = lookup.unreflect(formatBookMethod);
6. Вызов дескриптора метода
После того, как мы создали дескрипторы методов, следующим шагом будет их использование. В частности, класс MethodHandle
предоставляет 3 различных способа выполнения дескриптора метода: invoke(),
invokeWithArugments()
и invokeExact()
.
Начнем с опции вызова .
6.1. Вызов дескриптора метода
При использовании метода invoke()
мы принудительно фиксируем количество аргументов (арность), но разрешаем выполнение приведения и упаковки/распаковки аргументов и возвращаемых типов.
Давайте посмотрим, как можно использовать 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. Вызов с аргументами
Вызов дескриптора метода с помощью метода invokeWithArguments
является наименее ограничивающим из трех вариантов.
Фактически, он допускает вызов переменной арности в дополнение к приведению и упаковке/распаковке аргументов и возвращаемых типов.
На практике это позволяет нам создать список
целых чисел
, начиная с массива
значений 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. Вызов точного
Если мы хотим ограничить способ выполнения дескриптора метода (количество аргументов и их тип), мы должны использовать метод invokeExact()
.
На самом деле, он не обеспечивает никакого приведения к предоставленному классу и требует фиксированного количества аргументов.
Давайте посмотрим, как мы можем суммировать
два значения int
, используя дескриптор метода:
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
предназначены не только для работы с полями или объектами, но также и с массивами. Фактически, с помощью API asSpreader()
можно создать дескриптор метода расширения массива.
В этом случае дескриптор метода принимает аргумент массива, распространяя его элементы в качестве позиционных аргументов и, необязательно, длину массива.
Давайте посмотрим, как мы можем распространить дескриптор метода, чтобы проверить, равны ли элементы в массиве:
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. Улучшение дескриптора метода
После того, как мы определили дескриптор метода, его можно улучшить, привязав дескриптор метода к аргументу, фактически не вызывая его.
Например, в 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 в API MethodHandles было внесено несколько улучшений, чтобы упростить его использование.
Усовершенствования коснулись 3 основных тем:
- Функции поиска - позволяют искать классы из разных контекстов и поддерживают неабстрактные методы в интерфейсах.
- Обработка аргументов — улучшение функций свертывания аргументов, сбора аргументов и распределения аргументов.
- Дополнительные комбинации — добавление циклов (
loop
,whileLoop,
doWhileLoop…
) и улучшенная поддержка обработки исключений с помощьюtryFinally.
Эти изменения привели к нескольким дополнительным преимуществам:
- Повышенная оптимизация компилятора JVM.
- Сокращение экземпляров
- Включена точность использования API
MethodHandles.
Подробная информация о внесенных улучшениях доступна в документе MethodHandles
API Javadoc .
10. Заключение
В этой статье мы рассмотрели API MethodHandles
, что это такое и как мы можем их использовать.
Мы также обсудили, как это связано с API-интерфейсом Reflection, и, поскольку дескрипторы методов допускают низкоуровневые операции, лучше избегать их использования, если они не полностью соответствуют объему работы.
Как всегда, полный исходный код этой статьи доступен на Github .