JEP 274: Enhanced Method Handles
Улучшенные method handle
| Ответственный | Michael Haupt |
| Тип | Feature |
| Область | SE |
| Статус | Closed / Delivered |
| Выпуск | 9 |
| Компонент | core-libs / java.lang.invoke |
| Обсуждение | mlvm dash dev at openjdk dot java dot net |
| Рецензенты | Alex Buckley, Paul Sandoz, Vladimir Ivanov |
| Одобрен | John Rose |
| Создан | 2015/07/01 06:43 |
| Обновлён | 2017/05/17 01:03 |
| Задача | 8130227 |
Аннотация
Улучшить классы MethodHandle, MethodHandles и MethodHandles.Lookup пакета java.lang.invoke, чтобы упростить типичные сценарии использования и сделать возможными более эффективные оптимизации компилятора. Для этого предлагаются новые комбинаторы MethodHandle и доработка механизма поиска.
Цели
-
В классе
MethodHandlesпакетаjava.lang.invokeдобавить новые комбинаторыMethodHandleдля циклов и блоков try/finally. -
Дополнить классы
MethodHandleиMethodHandlesновыми комбинаторамиMethodHandleдля обработки аргументов. -
Реализовать в классе
MethodHandles.Lookupновые варианты поиска для методов интерфейсов и, необязательно, для конструкторов суперкласса.
Что не является целью
-
Расширения и улучшения на уровне VM, в частности оптимизации компилятора, не являются целью. Исключение составляет нативная функциональность, которая может потребоваться.
-
Расширения на уровне языка Java явно исключены из рассмотрения.
Мотивация
В ветке обсуждения в рассылке mlvm-dev (часть 1, часть 2) разработчики обсуждали возможные расширения классов MethodHandle, MethodHandles и MethodHandles.Lookup пакета java.lang.invoke. Эти расширения упростили бы реализацию типичных сценариев использования, а также позволили бы поддержать сценарии, которые считаются важными, но сейчас не поддерживаются.
Предлагаемые ниже расширения не только позволяют использовать API MethodHandle более лаконично, но и в некоторых случаях сокращают количество создаваемых экземпляров MethodHandle. Это, в свою очередь, поможет компилятору VM лучше выполнять оптимизации.
Комбинаторы для большего числа операторов
Циклы. Класс MethodHandles не предоставляет абстракций для построения циклов из экземпляров MethodHandle. Нужен способ строить циклы из MethodHandle, представляющих тело цикла, а также инициализацию и условие или счётчик.
Блоки try/finally. MethodHandles также не предоставляет абстракции для блоков try/finally. Нужен метод, который строит такие блоки из method handle, представляющих части try и finally.
Улучшенная обработка аргументов
Развёртывание аргументов. С помощью MethodHandle.asSpreader(Class<?> arrayType, int arrayLength) можно создать method handle, который распределяет содержимое последнего аргумента-массива по нескольким аргументам. Следует добавить ещё один метод asSpreader. Он позволит развернуть несколько аргументов, содержащихся в массиве в любом месте сигнатуры метода, в набор отдельных аргументов.
Сбор аргументов. Метод MethodHandle.asCollector(Class<?> arrayType, int arrayLength) создаёт method handle, который собирает последние arrayLength аргументов в массив. Сделать то же самое для нескольких аргументов в другом месте сигнатуры метода нельзя. Следует добавить ещё один метод asCollector, который это поддерживает.
Свёртка аргументов. Комбинатор свёртки foldArguments(MethodHandle target, MethodHandle combinator) не позволяет задать позицию в списке аргументов, с которой должна начинаться свёртка. Следует добавить аргумент позиции; число сворачиваемых аргументов неявно задаётся числом аргументов, которые принимает combinator.
Дополнительные функции поиска
Неабстрактные методы в интерфейсах. Сейчас сценарий, подобный следующему, завершится ошибкой во время выполнения в отмеченном месте:
interface I1 {
default void m() { System.err.println("I1.m"); }
}
interface I2 {
default void m() { System.err.println("I2.m"); }
}
class C implements I1, I2 {
public void m() { I2.super.m(); System.err.println("C.m"); }
}
public class IfcSuper {
public static void main(String[] args) throws Throwable {
C c = new C();
MethodHandles.Lookup l = MethodHandles.lookup();
MethodType t = MethodType.methodType(void.class);
// This lookup will fail with an IllegalAccessException.
MethodHandle di1m = l.findSpecial(I1.class, "m", t, C.class);
ci1m.invoke(c);
}
}
Однако должна быть возможность создавать MethodHandle, привязанные к неабстрактным методам интерфейсов.
Поиск классов. Наконец, API поиска должен позволять искать классы из разных контекстов, что сейчас невозможно. В области MethodHandles все необходимые проверки доступа выполняются во время поиска (а не во время выполнения, как в случае рефлексии). Классы передаются в виде их экземпляров .class. Нужен метод поиска, который облегчит поиск с определённым контролем над контекстом, например через границы модулей. Этот метод должен возвращать экземпляр Class с нужными ограничениями для дальнейшего использования в комбинаторах MethodHandle.
Описание
Комбинаторы для циклов
Наиболее общая абстракция цикла
Основные абстракции цикла включают инициализацию цикла, проверяемый предикат и вычисляемое тело. Наиболее общий комбинатор MethodHandle для создания цикла, который будет добавлен в MethodHandles, выглядит так:
MethodHandle loop(MethodHandle[]... clauses)
Создаёт method handle, представляющий цикл с несколькими переменными цикла, которые обновляются и проверяются на каждой итерации. Когда цикл завершается из-за одного из предикатов, выполняется соответствующий финализатор. Он возвращает результат цикла, который становится возвращаемым значением результирующего method handle.
Интуитивно каждый цикл состоит из одной или нескольких «секций», каждая из которых задаёт локальное значение итерации и/или выход из цикла. На каждой итерации цикла все секции выполняются по порядку. Секция может при необходимости обновлять свою переменную итерации; она также может при необходимости выполнять проверку и условный выход из цикла. Чтобы выразить эту логику с помощью method handle, каждая секция определяет четыре действия:
-
До выполнения цикла — инициализация переменной итерации или локальной переменной-инварианта цикла.
-
При выполнении секции — шаг обновления переменной итерации.
-
При выполнении секции — вычисление предиката для проверки выхода из цикла.
-
Если секция вызывает выход из цикла — выполнение финализатора для вычисления возвращаемого значения цикла.
Некоторые из этих частей секции могут быть опущены по определённым правилам; в этом случае предоставляется полезное поведение по умолчанию. Подробное описание см. ниже.
Каждая функция секции, за исключением инициализаторов секций, может видеть всё состояние цикла, потому что ей передаются все текущие значения переменных итерации, а также все входные параметры цикла. Большинству функций секций не понадобится вся эта информация, но формально они будут подключены так, как если бы использовался dropArguments.
Для заданного набора секций выполняется ряд проверок и корректировок, связывающих все части цикла. Они подробно описаны в шагах ниже. В этих шагах каждое вхождение слова «должен» соответствует месту, где может быть выброшено IllegalArgumentException, если входные данные комбинатора цикла не удовлетворяют требуемому ограничению. Термин «фактически идентичны» применительно к спискам типов параметров означает, что они должны быть идентичны или же один список должен быть собственным префиксом другого.
Шаг 0: определить структуру секций.
-
Массив секций (типа
MethodHandle[][]должен быть отличен отnullи содержать хотя бы один элемент. -
Массив секций не может содержать
nullили подмассивы длиннее четырёх элементов. -
Секции короче четырёх элементов обрабатываются так, как если бы они были дополнены элементами
nullдо длины четыре. Дополнение выполняется добавлением элементов в конец массива. -
Секции, состоящие только из
null, не учитываются. -
Каждая секция рассматривается как кортеж из четырёх функций, называемых «init», «step», «pred» и «fini».
Шаг 1A: определить переменные итерации.
-
Попарно проверить типы возвращаемых значений функций init и step, чтобы определить тип переменной итерации каждой секции.
-
Если обе функции опущены, использовать
void; иначе, если опущена одна из них, использовать тип возвращаемого значения другой; иначе использовать общий тип возвращаемого значения (они должны быть идентичны). -
Составить список типов возвращаемых значений (в порядке секций), исключив все вхождения
void. -
Этот список типов называется «общим префиксом».
Шаг 1B: определить параметры цикла.
-
Проверить списки параметров функций init.
-
Считается, что у опущенных функций init списки параметров
null. -
Все списки параметров функций init должны быть фактически идентичны.
-
Самый длинный список параметров (он обязательно единственный) называется «общим суффиксом».
Шаг 1C: определить тип возвращаемого значения цикла.
-
Проверить типы возвращаемых значений функций fini, не учитывая опущенные функции fini.
-
Если функций fini нет, использовать
voidкак тип возвращаемого значения цикла. -
Иначе использовать общий тип возвращаемого значения функций fini; все они должны быть идентичны.
Шаг 1D: проверить остальные типы.
-
Должна быть хотя бы одна неопущенная функция pred.
-
Каждая неопущенная функция pred должна иметь тип возвращаемого значения
boolean.
(Примечание по реализации: шаги 1A, 1B, 1C и 1D логически независимы друг от друга и могут выполняться в любом порядке.)
Шаг 2: определить списки параметров.
-
Списком параметров результирующего method handle цикла будет «общий суффикс».
-
Список параметров функций init будет приведён к «общему суффиксу». (Обратите внимание, что их списки параметров уже фактически идентичны общему суффиксу.)
-
Список параметров остальных функций (step, pred и fini) будет приведён к общему префиксу, за которым следует общий суффикс; такая последовательность называется «общей последовательностью параметров».
-
Список параметров каждой неопущенной функции, кроме init, должен быть фактически идентичен общей последовательности параметров.
Шаг 3: заполнить опущенные функции.
-
Если функция init опущена, использовать константную функцию соответствующего типа
null/ноль/false/void. (Для этой цели константныйvoid— это просто функция, которая ничего не делает и возвращаетvoid; её можно получить из другой константной функции преобразованием типа черезMethodHandle.asType type.) -
Если функция step опущена, использовать тождественную функцию типа переменной итерации секции. Перед параметром тождественной функции вставить отбрасываемые параметры-аргументы для отличных от
voidпеременных итерации предшествующих секций. (Так переменная цикла превратится в локальный инвариант цикла.) -
Если функция pred опущена, соответствующая функция fini тоже должна быть опущена.
-
Если функция pred опущена, использовать константную функцию
true. (Так цикл будет продолжаться, по крайней мере с точки зрения этой секции.) -
Если функция fini опущена, использовать константную функцию
null/ноль/false/voidс типом возвращаемого значения цикла.
Шаг 4: заполнить недостающие типы параметров.
-
На этом этапе список параметров каждой функции init фактически идентичен общему суффиксу, но некоторые списки могут быть короче. Для каждой функции init с коротким списком параметров дополнить конец списка с помощью отбрасываемых аргументов.
-
На этом этапе список параметров каждой функции, кроме init, фактически идентичен общей последовательности параметров, но некоторые списки могут быть короче. Для каждой такой функции с коротким списком параметров дополнить конец списка с помощью отбрасываемых аргументов.
Заключительные замечания.
-
После этих шагов все клаузы скорректированы: недостающие функции и аргументы добавлены.
-
У всех функций init общий список типов параметров, и такой же список будет у итогового дескриптора цикла.
-
У всех функций fini общий тип возвращаемого значения, и такой же тип будет у итогового дескриптора цикла.
-
У всех функций, кроме init, общий список типов параметров: это общая последовательность параметров, состоящая из переменных итерации (не
void), за которыми следуют параметры цикла. -
В каждой паре функций init и step типы возвращаемых значений совпадают.
-
Каждая функция, кроме init, сможет видеть текущие значения всех переменных итерации благодаря общему префиксу.
Выполнение цикла.
-
При вызове цикла его входные значения сохраняются в локальных переменных, чтобы передаваться (как общий суффикс) каждой функции клаузы. Эти локальные переменные инвариантны относительно цикла.
-
Каждая функция init выполняется в порядке клауз (ей передаётся общий суффикс), и значения, отличные от
void, сохраняются (как общий префикс) в локальных переменных. Эти локальные переменные изменяются в цикле (если только их функции step не являются тождественными функциями, как отмечено выше). -
При выполнении всех функций (кроме функций init) им будет передаваться общая последовательность параметров, состоящая из значений итерации, отличных от
void(в порядке клауз), а затем входных значений цикла (в порядке аргументов). -
Затем функции step и pred выполняются в порядке клауз (step перед pred), пока какая-либо функция pred не вернёт
false. -
Результат вызова функции step, отличный от
void, используется для обновления соответствующей переменной цикла. Обновлённое значение сразу видно всем последующим вызовам функций. -
Если функция pred возвращает
false, вызывается соответствующая функция fini, и полученное значение возвращается из цикла в целом.
Семантика MethodHandle l, возвращаемого из loop, такова:
l(arg*) =>
{
let v* = init*(arg*);
for (;;) {
for ((v, s, p, f) in (v*, step*, pred*, fini*)) {
v = s(v*, arg*);
if (!p(v*, arg*)) {
return f(v*, arg*);
}
}
}
}
На основе этой наиболее общей абстракции циклов в MethodHandles следует добавить несколько удобных комбинаторов. Они рассматриваются далее.
Простые циклы while и do-while
В MethodHandles будут добавлены следующие комбинаторы:
MethodHandle whileLoop(MethodHandle init, MethodHandle pred, MethodHandle body)
MethodHandle doWhileLoop(MethodHandle init, MethodHandle body, MethodHandle pred)
Семантика вызова объекта MethodHandle wl, возвращаемого из whileLoop, такова:
wl(arg*) =>
{
let r = init(arg*);
while (pred(r, arg*)) { r = body(r, arg*); }
return r;
}
Для MethodHandle dwl, возвращаемого из doWhileLoop, семантика такова:
dwl(arg*) =>
{
let r = init(arg*);
do { r = body(r, arg*); } while (pred(r, arg*));
return r;
}
Эта схема накладывает некоторые ограничения на сигнатуры трёх составляющих MethodHandle:
-
Тип возвращаемого значения инициализатора
initявляется также типом возвращаемого значения телаbodyи всего цикла, а также типом первого аргумента предикатаpredи телаbody. -
Тип возвращаемого значения предиката
predдолжен бытьboolean.
Циклы со счётчиком
Для удобства будут также предоставлены следующие комбинаторы циклов:
-
MethodHandle countedLoop(MethodHandle iterations, MethodHandle init, MethodHandle body)MethodHandlecl, возвращаемый изcountedLoop, имеет следующую семантику:cl(arg*) => { let end = iterations(arg*); let r = init(arg*); for (int i = 0; i < end; i++) { r = body(i, r, arg*); } return r; } -
MethodHandle countedLoop(MethodHandle start, MethodHandle end, MethodHandle init, MethodHandle body)MethodHandlecl, возвращаемый из этого вариантаcountedLoop, имеет следующую семантику:cl(arg*) => { let s = start(arg*); let e = end(arg*); let r = init(arg*); for (int i = s; i < e; i++) { r = body(i, r, arg*); } return r; }
В обоих случаях тип первого аргумента body должен быть int, а типы возвращаемых значений init и body, а также второго аргумента body должны совпадать.
Итерация по структурам данных
Кроме того, полезен комбинатор цикла для итерации:
-
MethodHandle iteratedLoop(MethodHandle iterator, MethodHandle init, MethodHandle body)MethodHandleit, возвращаемый изiteratedLoop, имеет следующую семантику:it(arg*) => { let it = iterator(arg*); let v = init(arg*); for (T t : it) { v = body(t, v, a); } return v; }
Замечания
Можно представить и другие удобные комбинаторы циклов.
Семантику continue легко эмулировать возвратом из тела, но вопрос о том, как эмулировать семантику break, остаётся открытым. Этого можно было бы добиться с помощью специального исключения (например, LoopMethodHandle.BreakException).
Комбинатор для блоков try/finally
Чтобы упростить построение функциональности с семантикой try/finally из MethodHandle, в MethodHandles будет добавлен следующий новый комбинатор:
MethodHandle tryFinally(MethodHandle target, MethodHandle cleanup)
Семантика вызова MethodHandle tf, возвращаемого из tryFinally, такова:
tf(arg*) =>
{
Throwable t;
Object r;
try {
r = target(arg*);
} catch (Throwable x) {
t = x;
throw x;
} finally {
r = cleanup(t, r, arg*);
}
return r;
}
То есть тип возвращаемого значения полученного MethodHandle будет таким же, как у дескриптора target. У target и cleanup должны быть совпадающие списки аргументов, с тем дополнением для cleanup, что он принимает один аргумент Throwable и результат — возможно, промежуточный. Если во время выполнения target было выброшено исключение, этот аргумент будет содержать это исключение.
Комбинаторы для обработки аргументов
В дополнение к существующему API в MethodHandles будут добавлены следующие методы:
-
Дополнение к классу
MethodHandle— новый метод экземпляра:MethodHandle asSpreader(int pos, Class<?> arrayType, int arrayLength)В сигнатуре результата на позиции
posожидатьarrayLengthаргументов типаarrayType. В результат вставить массив, поглощающийarrayLengthаргументовthisMethodHandle. Если в сигнатуреthisна этой позиции недостаточно аргументов или такой позиции в сигнатуре нет, выбросить соответствующее исключение.Например, если сигнатура
this—(Ljava/lang/String;IIILjava/lang/Object;)V, вызовasSpreader(int[].class, 1, 3)даст итоговую сигнатуру(Ljava/lang/String;[ILjava/lang/Object;)V. -
Дополнение к классу
MethodHandle— новый метод экземпляра:MethodHandle asCollector(int pos, Class<?> arrayType, int arrayLength)В сигнатуре
thisна позицииposожидать аргумент-массив. В сигнатуре результата на позицииposбудетarrayLengthаргументов типа элементов этого массива. Все аргументы доposне затрагиваются. Все аргументы послеposсдвигаются вправо наarrayLength. Ожидается, что распределяемые аргументы будут доступны в массиве во время выполнения; если это не так, выбрасываетсяArrayIndexOutOfBoundsException.Например, если сигнатура
this—(Ljava/lang/String;[ILjava/lang/Object;)V, вызовasCollector(int[].class, 1, 3)даст итоговую сигнатуру(Ljava/lang/String;IIILjava/lang/Object;)V. -
Дополнение к классу
MethodHandles— новый статический метод:MethodHandle foldArguments(MethodHandle target, int pos, MethodHandle combiner)Полученный
MethodHandleпри вызове будет действовать как существующий методfoldArguments(MethodHandle target, MethodHandle combiner), с той разницей, что существующий метод подразумевает позицию свёртки0, а предлагаемый новый метод позволяет указать позицию свёртки, отличную от0.Например, если сигнатура
target—(ZLjava/lang/String;ZI)I, а сигнатураcombiner—(ZI)Ljava/lang/String;, вызовfoldArguments(target, 1, combiner)даст итоговую сигнатуру(ZZI)I, и второй и третий аргументы (booleanиint) при каждом вызове будут свёрнуты вString.
Эти новые комбинаторы будут реализованы с использованием существующих абстракций и API. При необходимости будет изменён непубличный API.
Поиск
Реализация метода MethodHandles.Lookup.findSpecial(Class<?> refc, String name, MethodType type, Class<?> specialCaller) будет изменена так, чтобы можно было находить в интерфейсах методы, вызываемые через super. Хотя это не изменение API как такового, его документированное поведение меняется существенно.
Кроме того, класс MethodHandles.Lookup будет расширен следующими двумя методами:
-
Class<?> findClass(String targetName)Этот метод получает экземпляр
Class<?>, представляющий нужный целевой класс, идентифицируемыйtargetName. Поиск применяет ограничения, определённые неявным контекстом доступа. Если доступ невозможен, метод выбрасывает соответствующее исключение. -
Class<?> accessClass(Class<?> targetClass)Этот метод пытается получить доступ к заданному классу, применяя ограничения, определённые неявным контекстом доступа. Если доступ невозможен, метод выбрасывает соответствующее исключение.
Риски и допущения
Поскольку это чисто аддитивное расширение API, никакой код, который используют существующие клиенты API MethodHandle, не пострадает. Предлагаемые расширения также не зависят ни от каких других ведущихся разработок.
Для всех перечисленных выше расширений API будут предоставлены модульные тесты.
Зависимости
Этот JEP связан с JEP 193 (Variable Handles), и возможно некоторое пересечение, поскольку VarHandle зависят от API MethodHandle. Этот вопрос будет решён совместно с ответственным за JEP 193.
Задачу в JBS об улучшениях JSR 292 для сопровождающих выпусков можно считать отправной точкой для этого JEP, который отбирает из этой задачи те пункты, по которым достигнуто согласие.