跳到主要内容

JEP 274:增强的方法句柄

概括

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

目标

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

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

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

非目标

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

  • Java 语言级别的扩展显然超出了范围。

动机

在邮件列表上的一个线程mlvm-dev第 1 部分第 2 部分)中,开发人员讨论了包中MethodHandleMethodHandles、 和MethodHandles.Lookup类的可能扩展java.lang.invoke,以使常见用例的实现更容易,并且还允许被认为重要但但不重要的用例。目前不支持。

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

更多语句的组合器

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

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绑定到接口中的非抽象方法的 s。

**类查找。**最后,查找 API 应该允许从不同的上下文中查找_类_,但目前这是不可能的。在该MethodHandles区域中,所有必需的访问检查都是在查找时完成的(与运行时相反,如反射的情况)。类根据其.class实例进行传递。为了促进对上下文进行某种控制的查找,例如跨模块边界,应该有一种查找方法,可以提供Class具有正确限制的实例,以便在MethodHandle组合器中进一步使用。

描述

循环组合器

最通用的循环抽象

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

MethodHandle loop(MethodHandle[]... clauses)

构造一个表示循环的方法句柄,其中包含多个循环变量,这些循环变量在每次迭代时都会更新和检查。由于谓词之一而终止循环时,将运行相应的终结器并传递循环的结果,即结果句柄的返回值。

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

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

  • 当子句执行时,迭代变量的更新步骤。

  • 当子句执行时,谓词执行以测试 for 循环退出。

  • 如果子句导致循环退出,则执行终结器来计算循环的返回值。

根据某些规则,其中一些子句部分可以被省略,并且在这种情况下提供了有用的默认行为。请参阅下面的详细说明。

除子句初始值设定项外,每个子句函数都能够观察整个循环状态,因为它将传递_所有_当前迭代变量值以及所有传入的循环参数。大多数子句函数不需要所有这些信息,但它们将像通过 一样正式连接dropArguments

给定一组子句,需要执行许多检查和调整来连接循环的所有部分。下面的步骤详细说明了它们。在这些步骤中,单词“必须”的每次出现都对应于IllegalArgumentException如果循环组合器的输入不满足所需约束则可能被抛出的位置。术语“实际上相同”应用于参数类型列表,意味着它们必须相同,否则一个列表必须是另一个列表的正确前缀。

步骤0:确定子句结构。

  • 子句数组(类型MethodHandle[][]必须为非null且至少包含一个元素。

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

  • 短于四个元素的子句被视为用null长度为四个的元素填充。填充是通过将元素附加到数组来进行的。

  • 包含所有 s 的子句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:确定参数列表。

  • 生成的循环句柄的参数列表将是“公共后缀”。

  • init函数的参数列表将调整为“通用后缀”。 (请注意,它们的参数列表实际上与公共后缀相同。)

  • 非 init(step、pred 和 fini)函数的参数列表将调整为公共前缀后跟公共后缀,称为“公共参数序列”。

  • 每个非初始化、非省略的函数参数列表必须与公共参数序列实际上相同。

第三步:填写省略的功能。

  • 如果省略 init 函数,请使用适当null/zero/ false/void类型的常量函数。 (为此,常量void只是一个不执行任何操作并返回 的函数void;它可以通过 进行类型转换从另一个常量函数获得MethodHandle.asType type。)

  • 如果省略步骤函数,则使用子句迭代变量类型的恒等函数;对于void前面子句的非迭代变量,在恒等函数参数之前插入删除的参数参数。 (这会将循环变量变成局部循环不变量。)

  • 如果省略了 pred 函数,则相应的 fini 函数也必须省略。

  • 如果省略 pred 函数,则使用常量true函数。 (就本条款而言,这将使循环继续进行。)

  • 如果省略fini函数,则使用循环返回类型的常量null/zero/ false/函数。void

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

  • 此时,每个 init 函数参数列表实际上与公共后缀相同,但某些列表可能更短。对于每个具有短参数列表的 init 函数,通过删除参数来填充列表的末尾。

  • 此时,每个非 init 函数参数列表实际上与公共参数序列相同,但某些列表可能更短。对于每个具有短参数列表的非 init 函数,通过删除参数来填充列表的末尾。

最终观察结果。

  • 在这些步骤之后,所有子句都已通过提供省略的函数和参数进行了调整。

  • 所有 init 函数都有一个公共参数类型列表,最终循环句柄也将具有该列表。

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

  • void所有非 init 函数都有一个公共参数类型列表,它是(非)迭代变量后跟循环参数的公共参数序列。

  • 每对 init 和 step 函数的返回类型都一致。

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

循环执行。

  • 调用循环时,循环输入值保存在局部变量中,并传递(作为公共后缀)到每个子句函数。这些局部变量是循环不变的。

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

  • 所有函数执行(初始化函数除外)都将传递公共参数序列,其中包括非void迭代值(按子句顺序),然后是循环输入(按参数顺序)。

  • 然后,按子句顺序执行 step 和 pred 函数(step 在 pred 之前),直到 pred 函数返回false

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

  • 如果 pred 函数返回false,则调用相应的 fini 函数,并从整个循环中返回结果值。

MethodHandle lfrom返回的a的语义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 dwlfrom 返回的a doWhileLoop,语义如下:

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)

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

    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)

    MethodHandle cl从该变体返回的AcountedLoop具有以下语义:

    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)

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

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

为了方便使用 s 中的 try/finally 语义构建功能MethodHandle,将引入以下新组合器MethodHandles

MethodHandle tryFinally(MethodHandle target, MethodHandle cleanup)

MethodHandle tf调用a返回的语义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句柄的返回类型。 thetarget和 the都cleanup必须具有匹配的参数列表,其扩展名cleanup接受一个Throwable参数和(可能是中间的)结果。如果在执行期间引发异常target,则此参数将保存该异常。

用于参数处理的组合器

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

  • 添加到类MethodHandle- 新实例方法:

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

    在结果的签名中,在位置 处pos,期望arrayLength参数类型为arrayType。在结果中,插入一个使用 的参数arrayLength的数组this MethodHandle。如果 的签名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)Icombiner签名为(ZI)Ljava/lang/String;,则调用foldArguments(target, 1, combiner)将产生结果签名(ZZI)I,并且第二个和第三个 (boolean和) 参数将在每次调用时int折叠到 a 中。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(变量句柄)相关,并且可能存在一定程度的重叠,因为VarHandles 取决于MethodHandleAPI。这将与 JEP 193 的所有者合作解决。

关于维护版本的 JSR 292 增强功能的 JBS 问题可以被视为此 JEP 的起点,它从该问题中提炼出已达成一致的那些要点。