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
返回的MethodHandle
cl
的语义如下: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
返回的MethodHandle
cl
的语义如下: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
返回的MethodHandle
it
的语义如下: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
的参数。在结果中,插入一个接受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
中新增实例方法: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 的起点,从该问题中提取出已达成共识的要点。