Skip to content

JEP 274: Enhanced Method Handles | 增强的方法句柄

摘要

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

目标

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

  • 通过新的 MethodHandle 组合器,增强 MethodHandleMethodHandles 类来处理参数。

  • MethodHandles.Lookup 类中实现接口方法和(可选的)超级构造函数的新查找。

非目标

  • 除了可能需要的本机功能外,VM 级别的扩展和增强,特别是编译器优化,不是目标。

  • 明确不包括 Java 语言层面的扩展。

动机

mlvm-dev 邮件列表的一个线程中(part 1, part 2),开发人员讨论了对 java.lang.invoke 包中的 MethodHandleMethodHandlesMethodHandles.Lookup 类进行可能扩展的问题,以使常见用例更易实现,并允许当前不支持但被认为重要的用例。

下面提出的扩展不仅可以更简洁地使用 MethodHandle API,还可以减少某些情况下创建的 MethodHandle 实例的数量。这反过来将促进 VM 编译器的更好优化。

更多语句的组合器

循环。 MethodHandles 类没有提供从 MethodHandle 实例构造循环的抽象。应该有一种方式,可以从表示循环体、初始化和条件或计数的 MethodHandle 中构造循环。

try/finally 块。 MethodHandles 还没有提供用于 try/finally 块的抽象。应该提供一种从表示 tryfinally 部分的方法句柄构造这些块的方法。

更好的参数处理

参数展开。 使用 MethodHandle.asSpreader(Class<?> arrayType, int arrayLength),可以创建一个方法句柄,将一个 尾部 数组参数的内容展开成多个参数。应该提供额外的 asSpreader 方法,允许将方法签名中包含的数组中的一些参数展开为多个不同的参数。

参数收集。 MethodHandle.asCollector(Class<?> arrayType, int arrayLength) 方法产生一个句柄,将 尾部arrayLength 个参数收集到一个数组中。在方法签名的其他位置实现相同效果的方法。应该提供一个支持此功能的额外 asCollector 方法。

参数折叠。 折叠组合器 foldArguments(MethodHandle target, MethodHandle combinator) 并不能控制参数列表中开始折叠的位置。应添加一个位置参数;折叠的参数数量隐含为 combinator 接受的参数数量。

更多的查找功能

接口中的非抽象方法。 目前,这样的用例在指定位置会在运行时失败:

java
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 中,如下所示:

java
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 的语义如下:

javascript
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 :

java
MethodHandle whileLoop(MethodHandle init, MethodHandle pred, MethodHandle body)

MethodHandle doWhileLoop(MethodHandle init, MethodHandle body, MethodHandle pred)

调用从 whileLoop 返回的 MethodHandle 对象 wl 的语义如下:

javascript
wl(arg*) =>
{
    let r = init(arg*);
    while (pred(r, arg*)) { r = body(r, arg*); }
    return r;
}

对于从 doWhileLoop 返回的 MethodHandle dwl,语义如下:

javascript
dwl(arg*) =>
{
    let r = init(arg*);
    do { r = body(r, arg*); } while (pred(r, arg*));
    return r;
}

此方案对三个组成 MethodHandle 的签名施加了一些限制:

  1. 初始化器 init 的返回类型也是 body 和整个循环的返回类型,以及谓词 predbody 的第一个参数的类型。

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

计数循环

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

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

    countedLoop 返回的 MethodHandle cl 的语义如下:

    javascript
    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 的语义如下:

    javascript
    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 的语义如下:

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

为了通过 MethodHandletry / finally 语义构建功能,将在 MethodHandles 中引入以下新的组合器:

java
MethodHandle tryFinally(MethodHandle target, MethodHandle cleanup)

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

javascript
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 中新增实例方法:

    java
    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 中新增实例方法:

    java
    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 中新增静态方法:

    java
    MethodHandle foldArguments(MethodHandle target, int pos, MethodHandle combiner)

    当调用结果 MethodHandle 时,它将像现有方法 foldArguments(MethodHandle target, MethodHandle combiner) 一样工作,不同之处在于已存在的方法将隐式指定了折叠位置为 0,而新提议的方法允许指定折叠位置为非 0

    例如,如果 target 的签名是 (ZLjava/lang/String;ZI)Icombiner 的签名是 (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 的起点,从该问题中提取出已达成共识的要点。