Перейти к основному содержимому

Дескрипторы методов в Java

· 8 мин. чтения

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 .