跳到主要内容

JEP 274:增强的方法句柄

QWen Max 中英对照

概述

增强 java.lang.invoke 包中的 MethodHandleMethodHandlesMethodHandles.Lookup 类,通过引入新的 MethodHandle 组合器和查找细化功能,简化常见用例并实现更好的编译器优化。

目标

  • java.lang.invoke 包的 MethodHandles 类中,为循环和 try/finally 块提供新的 MethodHandle 组合器。

  • 使用用于参数处理的新 MethodHandle 组合器增强 MethodHandleMethodHandles 类。

  • MethodHandles.Lookup 类中实现用于接口方法的新查找功能,并可选择性地支持超类构造函数。

非目标

  • 除了可能需要的原生功能外,虚拟机级别的扩展和增强,特别是编译器优化,并非目标。

  • Java 语言级别的扩展明确不在范围内。

动机

mlvm-dev 邮件列表的一个讨论串中(第一部分第二部分),开发者们讨论了对 java.lang.invoke 包中的 MethodHandleMethodHandlesMethodHandles.Lookup 类进行可能的扩展,以使常见用例的实现更加容易,同时也允许支持那些被认为重要但目前尚未支持的用例。

下面提出的扩展不仅允许更简洁地使用 MethodHandle API,而且在某些情况下还减少了创建的 MethodHandle 实例的数量。这反过来将有助于虚拟机编译器进行更好的优化。

更多语句的组合器

循环。 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 实例传递。为了在一定上下文控制下进行查找,例如跨模块边界,应该有一个查找方法,能够提供一个带有适当限制的 Class 实例,以供在 MethodHandle 组合器中进一步使用。

描述

循环组合器

最通用的循环抽象

循环的核心抽象包括循环的初始化、要检查的谓词以及要评估的主体。用于创建循环的最通用的 MethodHandle 组合器,将被添加到 MethodHandles 中,其形式如下:

MethodHandle loop(MethodHandle[]... clauses)

构造一个方法句柄,表示带有多个循环变量的循环,这些变量在每次迭代时都会被更新和检查。当由于某个谓词导致循环终止时,会运行相应的终结器并返回循环的结果,该结果是所生成句柄的返回值。

直观上,每个循环都由一个或多个“子句”组成,每个子句指定一个局部迭代值和/或循环退出条件。循环的每次迭代按顺序执行每个子句。子句可以选择更新其迭代变量;也可以选择执行测试并根据条件退出循环。为了用方法句柄来表达这种逻辑,每个子句将确定四个动作:

  • 在循环执行之前,初始化一个迭代变量或循环不变的局部变量。

  • 当一个子句执行时,对迭代变量进行更新操作。

  • 当一个子句执行时,执行谓词以测试是否退出循环。

  • 如果一个子句导致循环退出,则执行终结操作以计算循环的返回值。

根据某些规则,这些子句部分可能会被省略,并且在这种情况下会提供有用的默认行为。详见下文的详细描述。

每个子句函数(clause initializers 除外)都能够观察整个循环状态,因为它将接收 所有 当前迭代变量的值以及所有传入的循环参数。大多数子句函数并不会需要所有这些信息,但它们会通过类似 dropArguments 的方式在形式上建立连接。

给定一组子句,会执行多项检查和调整以连接循环的所有部分。这些步骤在下面详细列出。在这些步骤中,单词“must”(必须)的每次出现都对应一个地方,如果输入到循环组合器的参数不满足所需的约束条件,则可能会抛出 IllegalArgumentException 异常。术语“effectively identical”(实际上相同)应用于参数类型列表时,意味着它们必须完全相同,或者其中一个列表必须是另一个列表的正确前缀。

步骤 0:确定子句结构。

  • 子句数组(类型为 MethodHandle[][])必须是非空的,并且至少包含一个元素。

  • 子句数组不得包含 null 或长度超过四个元素的子数组。

  • 长度小于四个元素的子句会被视为用 null 元素填充至长度为四。填充的方式是向数组追加元素。

  • 所有元素均为 null 的子句将被忽略。

  • 每个子句被视为一个由四个函数组成的四元组,分别称为“init”、“step”、“pred”和“fini”。

步骤 1A:确定迭代变量。

  • 检查初始化函数和步进函数的返回类型,两两成对,以确定每个子句的迭代变量类型。

  • 如果两个函数都被省略,则使用 void;如果其中一个被省略,则使用另一个函数的返回类型;否则使用它们共同的返回类型(必须完全相同)。

  • 按子句顺序形成返回类型的列表,同时忽略所有 void 的出现。

  • 该类型列表被称为“共同前缀”。

步骤 1B:确定循环参数。

  • 检查初始化函数的参数列表。

  • 被省略的初始化函数被视为具有 null 参数列表。

  • 所有初始化函数的参数列表必须实质上相同。

  • 最长的参数列表(必然是唯一的)被称为“共同后缀”。

步骤 1C:确定循环返回类型。

  • 检查 fini 函数的返回类型,忽略省略的 fini 函数。

  • 如果没有 fini 函数,则使用 void 作为循环返回类型。

  • 否则,使用 fini 函数的公共返回类型;它们必须全部相同。

步骤 1D:检查其他类型。

  • 必须至少有一个未被忽略的 pred 函数。

  • 每个未被忽略的 pred 函数必须具有 boolean 返回类型。

(实现说明:步骤 1A、1B、1C、1D 在逻辑上彼此独立,可以按任何顺序执行。)

步骤 2:确定参数列表。

  • 结果循环句柄的参数列表将是“共同后缀”。

  • 初始化函数的参数列表将调整为“共同后缀”。(注意,它们的参数列表已经与共同后缀有效相同。)

  • 非初始化(步骤、谓词和结束)函数的参数列表将调整为共同前缀后接共同后缀,称为“共同参数序列”。

  • 每个非初始化且非省略的函数参数列表必须与共同参数序列有效相同。

步骤 3:填写省略的函数。

  • 如果省略了初始化函数(init function),则使用适当类型的 null/零/false/void 常量函数。(在此情况下,一个常量 void 可以简单理解为一个什么都不做并返回 void 的函数;它可以通过 MethodHandle.asType type 类型转换从另一个常量函数获得。)

  • 如果省略了步进函数(step function),则使用与该子句的迭代变量类型相对应的恒等函数;在恒等函数参数之前插入前一子句中非 void 类型的迭代变量所对应的被丢弃参数。(这将把循环变量转化为局部循环不变量。)

  • 如果省略了谓词函数(pred function),则相应的最终函数(fini function)也必须省略。

  • 如果省略了谓词函数(pred function),则使用一个常量 true 函数。(这将使循环继续进行,就本子句而言。)

  • 如果省略了最终函数(fini function),则使用与循环返回类型相对应的 null/零/false/void 常量函数。

步骤 4:填写缺失的参数类型。

  • 此时,每个初始化函数的参数列表实际上与公共后缀相同,但某些列表可能较短。对于每个参数列表较短的初始化函数,通过删除参数来补齐列表末尾。

  • 此时,每个非初始化函数的参数列表实际上与公共参数序列相同,但某些列表可能较短。对于每个参数列表较短的非初始化函数,通过删除参数来补齐列表末尾。

最终观察结果。

  • 经过这些步骤后,所有子句均已通过补充省略的函数和参数进行了调整。

  • 所有初始化函数都具有一个共同的参数类型列表,最终的循环句柄也将具有该列表。

  • 所有结束函数都具有一个共同的返回类型,最终的循环句柄也将具有该类型。

  • 所有非初始化函数都具有一个共同的参数类型列表,即(非 void 类型的)迭代变量后跟循环参数的通用参数序列。

  • 每对初始化函数和步进函数在其返回类型上保持一致。

  • 每个非初始化函数将能够通过通用前缀观察所有迭代变量的当前值。

循环执行。

  • 当循环被调用时,循环的输入值会被保存在局部变量中,这些值(作为通用后缀)将传递给每个子句函数。这些局部变量是循环不变量。

  • 每个初始化函数会按照子句顺序执行(传递通用后缀),并将非 void 的值保存(作为通用前缀)到局部变量中。这些局部变量是循环变化的(除非它们的步进函数是恒等函数,如上所述)。

  • 所有函数执行(除了初始化函数)都会传递通用参数序列,该序列由非 void 的迭代值(按子句顺序)和循环输入值(按参数顺序)组成。

  • 然后按照子句顺序执行步进函数和谓词函数(步进函数在谓词函数之前),直到某个谓词函数返回 false

  • 从步进函数调用得到的非 void 结果用于更新相应的循环变量。更新后的值对所有后续函数调用立即可见。

  • 如果某个谓词函数返回 false,则会调用对应的结束函数,并将其结果作为整体从循环中返回。

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 的签名施加了一些限制:

  1. 初始化器 init 的返回类型,也是主体 body 的返回类型和整个循环的返回类型,同时也是谓词 pred 和主体 body 的第一个参数的类型。

  2. 谓词 pred 的返回类型必须为 boolean

计数循环

为了方便起见,还将提供以下循环组合子:

  • MethodHandle countedLoop(MethodHandle iterations, MethodHandle init, MethodHandle body)

    countedLoop 返回的 MethodHandle cl 具有以下语义:

    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)

    从此变体的 countedLoop 返回的 MethodHandle cl 具有以下语义:

    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,并且 initbody 的返回类型以及 body 的第二个参数必须相同。

数据结构的迭代

此外,用于迭代的循环组合器很有帮助:

  • MethodHandle iteratedLoop(MethodHandle iterator, MethodHandle init, MethodHandle body)

    iteratedLoop 返回的 MethodHandle it 具有以下语义:

    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 块的组合器

为了便于从 MethodHandle 构建具有 try/finally 语义的功能,将在 MethodHandles 中引入以下新的组合器:

MethodHandle tryFinally(MethodHandle 目标, MethodHandle 清理)

tryFinally 返回的 MethodHandle tf 的调用语义如下:

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 句柄的返回类型。targetcleanup 都必须具有匹配的参数列表,不过 cleanup 有一个扩展:它接受一个 Throwable 类型的参数以及可能的中间结果。如果在 target 执行期间抛出了异常,该参数将保存该异常。

用于参数处理的组合子

作为对 MethodHandles 中现有 API 的补充,将引入以下方法:

  • MethodHandle 类的新增内容 - 新的实例方法:

    MethodHandle asSpreader(int pos, Class<?> arrayType, int arrayLength)

    在结果的签名中,在位置 pos,期望有 arrayLength 个类型为 arrayType 的参数。在结果中,插入一个数组,该数组将消耗 this MethodHandle 中的 arrayLength 个参数。如果 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,并且第二个和第三个(booleanint)参数将在每次调用时被折叠为一个 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 的起点,它从该问题中提炼出已达成共识的要点。