JEP 274: Enhanced Method Handles | 增强的方法句柄
摘要
增强 java.lang.invoke 包中的 MethodHandle 、 MethodHandles 和 MethodHandles.Lookup 类,以简化常见用例并通过新的 MethodHandle 组合器和查找细化来实现更好的编译器优化。
目标
在
java.lang.invoke包中的MethodHandles类中,为循环和 try/finally 块提供新的MethodHandle组合器。通过新的
MethodHandle组合器,增强MethodHandle和MethodHandles类来处理参数。在
MethodHandles.Lookup类中实现接口方法和(可选的)超级构造函数的新查找。
非目标
除了可能需要的本机功能外,VM 级别的扩展和增强,特别是编译器优化,不是目标。
明确不包括 Java 语言层面的扩展。
动机
在 mlvm-dev 邮件列表的一个线程中(part 1, part 2),开发人员讨论了对 java.lang.invoke 包中的 MethodHandle 、 MethodHandles 和 MethodHandles.Lookup 类进行可能扩展的问题,以使常见用例更易实现,并允许当前不支持但被认为重要的用例。
下面提出的扩展不仅可以更简洁地使用 MethodHandle API,还可以减少某些情况下创建的 MethodHandle 实例的数量。这反过来将促进 VM 编译器的更好优化。
更多语句的组合器
循环。 MethodHandles 类没有提供从 MethodHandle 实例构造循环的抽象。应该有一种方式,可以从表示循环体、初始化和条件或计数的 MethodHandle 中构造循环。
try/finally 块。 MethodHandles 还没有提供用于 try/finally 块的抽象。应该提供一种从表示 try 和 finally 部分的方法句柄构造这些块的方法。
更好的参数处理
参数展开。 使用 MethodHandle.asSpreader(Class<?> arrayType, int arrayLength),可以创建一个方法句柄,将一个 尾部 数组参数的内容展开成多个参数。应该提供额外的 asSpreader 方法,允许将方法签名中包含的数组中的一些参数展开为多个不同的参数。
参数收集。 MethodHandle.asCollector(Class<?> arrayType, int arrayLength) 方法产生一个句柄,将 尾部 的 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 实例传递的。为了方便具有对上下文的某种控制,例如跨模块边界查找,应该有一个查找方法,提供具有适用于在 MethodHandle 组合器中进一步使用的正确限制的 Class 实例。
描述
循环的组合子
最通用的循环抽象
循环的核心抽象包括循环的初始化、检查的谓词和待评估的主体。用于创建循环的最通用的 MethodHandle 组合子,将被添加到 MethodHandles 中,如下所示:
MethodHandle loop(MethodHandle[]... clauses)构造一个表示具有多个循环变量的循环的方法句柄,这些变量在每次迭代时被更新和检查。当由于谓词之一而终止循环时,将运行相应的终结器并生成循环的结果,即返回所得句柄的返回值。
直观地说,每个循环都由一个或多个 "子句" 组成,每个子句指定一个本地迭代值和 / 或循环退出条件。循环的每次迭代按顺序执行每个子句。子句可以可选择地更新其迭代变量;它还可以可选择地执行测试和条件循环退出。为了以方法句柄的术语来表达这个逻辑,每个子句将确定四个动作:
- 在循环执行之前,进行迭代变量或循环不变局部变量的初始化。
- 当一个子句执行时,迭代变量的更新步骤。
- 当一个子句执行时,执行谓词以测试循环是否退出。
- 如果一个子句导致循环退出,则执行终结器来计算循环的返回值。
根据某些规则,可以省略这些子句的部分。在这种情况下,提供有用的默认行为。详细描述请参见下文。
除了子句初始化器之外,每个子句函数都可以观察整个循环状态,因为它们将被传递所有当前迭代变量值以及所有传入的循环参数。大多数子句函数不需要所有这些信息,但它们将被形式上连接,就好像通过 dropArguments 连接一样。
给定一组子句,有一系列检查和调整的步骤来连接循环的所有部分。下面详细说明了这些步骤。在这些步骤中,每个单词 "必须" 对应一个地方,如果循环组合子的输入不满足所需的约束条件,则可能抛出 IllegalArgumentException。应用于参数类型列表的术语 "实际上相同" 意味着它们必须相同,否则一个列表必须是另一个列表的前缀。
步骤 0:确定子句结构。
- 子句数组(类型为
MethodHandle[][])必须为非空,并且包含至少一个元素。 - 子句数组不能包含
null值或长度大于四的子数组。 - 长度小于四的子句被视为填充了
null元素以使其长度为四。通过将元素追加到数组来进行填充。 - 包含全部
null的子句被忽略。 - 每个子句被视为一个包含四个函数的四元组,分别称为 "init"、"step"、"pred" 和 "fini"。
步骤 1A:确定迭代变量。
- 检查初始化和步骤函数的返回类型,一一对应,以确定每个子句的迭代变量类型。
- 如果两个函数都省略,则使用
void;否则,如果其中一个省略,则使用另一个的返回类型;否则使用公共返回类型(它们必须相同)。 - 根据子句顺序形成返回类型列表,省略所有
void出现的情况。 - 此类型列表称为 "公共前缀"。
步骤 1B:确定循环参数。
- 检查初始化函数的参数列表。
- 被省略的初始化函数被视为具有
null参数列表。 - 所有初始化函数的参数列表必须实际上相同。
- 最长的参数列表(必然唯一)称为 "公共后缀"。
步骤 1C:确定循环返回类型。
- 检查终结器函数的返回类型,忽略省略的终结器函数。
- 如果没有终结器函数,则使用
void作为循环的返回类型。 - 否则,使用终结器函数的公共返回类型;它们必须全部相同。
步骤 1D:检查其他类型。
- 必须至少有一个非省略的谓词函数。
- 每个非省略的谓词函数必须具有
boolean返回类型。
(实现说明:步骤 1A、1B、1C、1D 在逻辑上是相互独立的,可以按任意顺序执行。)
步骤 2:确定参数列表。
- 结果循环句柄的参数列表将是 "公共后缀"。
- 初始化函数的参数列表将调整为 "公共后缀"。(注意,它们的参数列表已经实际上与公共后缀相同。)
- 非初始化(步骤、谓词和终结器)函数的参数列表将调整为公共前缀后跟公共后缀,称为 "公共参数序列"。
- 每个非初始化、非省略的函数的参数列表必须实际上与公共参数序列相同。
步骤 3:填充省略的函数。
- 如果省略了初始化函数,则使用适当的
null/ 零 /false/void类型的常量函数。(对于此目的,一个常量void仅仅是一个不执行任何操作并返回void的函数;通过MethodHandle.asType type的类型转换可以从另一个常量函数获得它。) - 如果省略了步骤函数,则使用子句的迭代变量类型的恒等函数;在非
void迭代变量的前面子句之前插入被丢弃的参数。(这将把循环变量转换为局部循环不变量。) - 如果省略了谓词函数,则相应的终结器函数也必须被省略。
- 如果省略了谓词函数,则使用常量
true函数。(这将保持循环进行,就这个子句而言。) - 如果省略了终结器函数,则使用循环返回类型的常量
null/ 零 /false/void函数。
步骤 4:填充缺失的参数类型。
- 在这一点上,每个初始化函数的参数列表实际上与公共后缀相同,但某些列表可能较短。对于具有短参数列表的每个初始化函数,通过丢弃参数来填充列表的末尾。
- 在这一点上,每个非初始化函数的参数列表实际上与公共参数序列相同,但某些列表可能较短。对于具有短参数列表的每个非初始化函数,通过丢弃参数来填充列表的末尾。
最后的观察。
- 在这些步骤之后,通过提供省略的函数和参数来调整了所有子句。
- 所有初始化函数具有一个公共参数类型列表,最终的循环句柄也将具有该参数类型列表。
- 所有终结器函数具有一个公共返回类型,最终的循环句柄也将具有该返回类型。
- 所有非初始化函数具有一个公共参数类型列表,即为公共参数序列,由(非
void)迭代变量和循环参数组成。 - 每一对初始化和步骤函数在它们的返回类型上达成一致。
- 每个非初始化函数将能够通过公共前缀观察所有迭代变量的当前值。
循环执行。
当调用循环时,循环输入值被保存在本地,可以传递(作为公共后缀)到每个子句函数。这些本地变量是循环不变的。
每个初始化函数按子句顺序执行(传递公共后缀),并且非
void值被保存(作为公共前缀)到本地变量。这些本地变量是循环变化的(除非它们的步骤是恒等函数,如上所述)。所有函数执行(除了初始化函数)将传递公共参数序列,包括非
void迭代值(按子句顺序)和然后是循环输入(按参数顺序)。然后,按子句顺序执行步骤和
pred函数(步骤在pred之前),直到pred函数返回false。来自步骤函数调用的非
void结果用于更新相应的循环变量。更新后的值立即对所有后续函数调用可见。如果
pred函数返回false,则调用相应的fini函数,并从整个循环返回结果值。
从 loop 返回的 MethodHandle l 的语义如下:
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)调用从 whileLoop 返回的 MethodHandle 对象 wl 的语义如下:
wl(arg*) =>
{
let r = init(arg*);
while (pred(r, arg*)) { r = body(r, arg*); }
return r;
}对于从 doWhileLoop 返回的 MethodHandle dwl,语义如下:
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)从
countedLoop返回的MethodHandlecl的语义如下:javascriptcl(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)此变体的
countedLoop返回的MethodHandlecl的语义如下:javascriptcl(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)从
iteratedLoop返回的MethodHandleit的语义如下:javascriptit(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 块的组合器
为了通过 MethodHandle 从 try / finally 语义构建功能,将在 MethodHandles 中引入以下新的组合器:
MethodHandle tryFinally(MethodHandle target, MethodHandle cleanup)调用从 tryFinally 返回的 MethodHandle 的语义如下:
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 过程中抛出异常,该参数将保存异常信息。
参数处理的组合器
作为对 MethodHandles 现有 API 的补充,将引入以下方法:
在类
MethodHandle中新增实例方法:javaMethodHandle asSpreader(int pos, Class<?> arrayType, int arrayLength)在结果的签名中,在位置
pos,期望有arrayLength个类型为arrayType的参数。在结果中,插入一个接受thisMethodHandle的数组,并消耗arrayLength个参数。如果this的签名在该位置没有足够的参数,或者该位置在签名中不存在,则会抛出适当的异常。例如,如果
this的签名是(Ljava/lang/String;IIILjava/lang/Object;)V,调用asSpreader(int[].class, 1, 3)将导致结果的签名是(Ljava/lang/String;[ILjava/lang/Object;)V。在类
MethodHandle中新增实例方法:javaMethodHandle 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中新增静态方法:javaMethodHandle 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 扩展,不会对使用 MethodHandle API 的现有客户端代码产生负面影响。提议的扩展也不依赖于任何其他正在进行的开发。
将提供所有上述 API 扩展的单元测试。
依赖关系
本 JEP 与 JEP 193(变量句柄) 相关,并且可能存在一定的重叠,因为 VarHandle 依赖于 MethodHandle API。这将在与 JEP 193 的所有者合作中解决。
JBS 问题 JSR 292 增强维护版本 可以视为本 JEP 的起点,从该问题中提取出已达成共识的要点。