MethodHandle入门

0.前言

java.lang.invoke包中的MethodHandle API是一个功能强大的反射和代码生成 API,并且还具有广泛的 JIT 支持。在这篇博文中,我将概述此 API。我将介绍 API 的一些最常用部分,但这不是一份全面的指南。它旨在为您提供一个起点,让您可以开始自行学习更多内容。鼓励读者自己尝试这些示例。

1.什么是MethodHandle?

MethodHandle是一个包装了固定 Java目标方法的java.lang.invoke.MethodHandle对象。顾名思义,它是 Java 方法的“句柄”。可以通过方法句柄对象调用目标方法。

在很多情况下,我们可以使用函数式接口来表示对某些可调用代码的引用,例如包中的某个接口java.util.function。但是,我们不能依赖泛型和函数式接口来表示采用可变数量和不同类型的参数的方法。我们能做的最好的就是使用 varargs 方法,它将参数收集到数组中,但是这会导致创建 varargs 数组的开销、装箱和拆箱原始值的开销以及从数组中存储和加载值的开销。我们也不能同时表示一个返回值的方法和一个不返回任何值的方法。有变通的的解决方法是void,但这些仍然需要一种方法来返回一些值,通常是null。

interface GenericFunction<R> {
    R apply(Object... args);
}

static int m1(int x, int y) {
    return x + y;
}

static void m2() {}

static void main(String[] __) {
    GenericFunction<Integer> m2Ref = args -> { return m1((int) args[0], (int) args[1]); };
    int result = m2Ref.apply(1, 2); // arguments boxed into Object[]
    // result has to be unboxed

    // have to explicitly return 'null'
    GenericFunction<Void> m1Ref = args -> { m2(); return null; };
}

另一方面,方法句柄在装箱和拆箱时没有相同的限制(稍后会详细介绍)。

实际上,方法句柄最适用于表示在运行时生成代码的 API 的结果,其中参数的数量和类型是事先不知道的。例如,方法句柄用于在外部函数和内存访问 (FFM) API 中实现对本机函数的引用。用于 java.lang.foreign.Linker::downcallHandle此目的的方法接受 FunctionDescriptor并返回MethodHandle可用于调用 C 函数的实例。链接器 API 需要提供对任何 C 函数的访问,因此它需要一种可用于表示对具有不同元数、具有不同参数和返回类型的函数的引用的类型,同时避免可变参数和装箱与拆箱原始值的开销。在这种情况下MethodHandle是一个很好的选择。

为特定 Java 方法创建MethodHandle的最简单方法之一是执行方法句柄查找。首先,我们必须创建一个MethodHandles.Lookup对象。这可以使用工厂方法来完成MethodHandles::lookup 。然后,我们可以使用 Lookup 中的一种findXXX方法来查找现有的 Java 方法,如以下测试程序所示:

import java.lang.invoke.*;

public class Main {
    public static void main(String[] args) throws Throwable {
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        MethodType typeOfTarget = MethodType.methodType(void.class);
        MethodHandle targetMh = lookup.findStatic(Main.class, "target", typeOfTarget);

        targetMh.invoke(); // prints 'invoking target'
    }

    public static void target() {
        System.out.println("invoking target");
    }
}

这里我们声明了一个静态target方法,然后我们可以使用findStatic来查找它。我们将保存该target方法的类、其名称以及表示为 的类型java.lang.invoke.MethodType传递给findStatic。这会返回该 target方法的MethodHandle ,然后我们通过调用invoke来调用它。还要注意,该invoke方法会抛出Throwable,所以我们通过在main方法的throws子句中声明它来处理这个问题。

Lookup中的其他findXXX方法可用于查找其他类型的方法。方法findVirtual对应于invokevirtual字节码,可用于查找实例(即非静态)方法,也称为虚方法。findConstructor可用于查找构造函数。还有find(Static)Getter 和find(Static)Setter用于读取和写入字段。请注意,这些不是查找 getX或setX方法,而是查找直接获取或设置字段的概念方法。这就像获取或设置字段的方法是动态生成的。这些对应于putfield、getfield、putstatic、getstatic和字节码。这些只是几个例子。

您可能已经意识到,MethodHandle从概念上讲, 与一个 java.lang.reflect.Method 非常相似。但是,API 更精简,因为方法句柄不反映目标方法的访问修饰符或其他内容(例如注释)。方法句柄实际上只有两个功能:它具有类型,并且您可以调用它。 MethodHandle中的其余方法称为连接符(combinators) ,允许客户端基于此方法句柄创建其他方法句柄。我们将在后面的章节中讨论连接符(combinators)。

和 一样 ,在最简单的用例中,可以使用方法句柄来反射调用 Java 目标方法。然而,这两个 API 之间也存在一些关键差异。

2.访问检查

调用java.lang.reflect.Method中的invoke方法时,该方法会执行访问检查。注释也暗示了这一点@CallerSensitive。此信息注释表明该方法将在调用时检查其调用者。实际上,该invoke方法将进行简短的堆栈遍历以查找调用者的类(尽管 JIT 可能会优化此堆栈遍历)。然后,该方法检查调用者是否具有调用目标方法所需的访问权限。

另一方面,方法句柄在调用时不进行任何访问检查(因此不需要对调用者敏感)。相反,访问检查是在查找方法句柄时执行的。您会注意到,方法是对调用者敏感的。调用时,调用MethodHandles::lookup者lookup类被捕获为查找类。然后在执行方法句柄查找时使用此查找类来检查它是否具有调用目标方法所需的访问权限。这允许客户端在创建方法句柄时进行一次访问检查,之后通过避免在调用方法句柄时进行访问检查来获得更好的性能。

这也意味着,如果您拥有方法句柄对象,您就有能力调用它。因此,我们说方法句柄是一种“能力”。通过共享方法句柄对象,可以与通常无法访问目标方法的其他代码共享此功能,以允许该代码调用目标方法。这是您可能想要使用方法句柄的原因之一。

3.异常处理

java.lang.reflect.Method中的invoke方法将底层 Java 方法抛出的异常包装到InvocationTargetException中,而MethodHandle的invoke方法将直接传播异常而不进行包装。这也是invoke抛出Throwable的原因。这解释了需要从目标方法传播的任何可能的可抛出对象。

invoke抛出Throwable异常这一事实可能看起来很麻烦,因为我们需要以某种方式处理它(Throwable)。但是,如果目标方法未声明任何已检查异常,我们可以假设invoke在实践中也永远不会抛出已检查异常。因此,在大多数情况下,我们可以将调用包装invoke在try/catch块中,并在 catch 块中抛出未检查异常,例如:

MethodHandle mh = ...
try {
    mh.invoke();
} catch (Throwable t) {
    throw new RuntimeException("Should not happen", t);

invoke不包装抛出的异常这一事实也意味着可以使用方法句柄来干净地实现方法,而不必担心为了传播它们而解开抛出的异常。这也是您可能想要使用方法句柄的另一个原因。

4.签名多态性

java.lang.reflect.Method的invoke方法声明如下(省略了一些细节):

@CallerSensitive
public Object invoke(Object obj, Object... args)
    throws IllegalAccessException, InvocationTargetException

第一个参数表示虚拟方法调用的接收方实例。对于静态方法,它将被忽略。

乍一看,MethodHandle的invoke方法该invoke方法可能看起来非常相似:

public final native @PolymorphicSignature Object invoke(Object... args) throws Throwable;

但是,此声明的返回类型和参数类型完全是虚构的。正如@PolymorphicSignature注释所暗示的那样,此方法是签名多态的。

此方法非常特殊,在 Java 语言规范中它甚至是特例。签名多态方法是一种其类型不是由方法声明决定,而是由方法调用站点(代码中调用方法的位置)决定的方法。换句话说,invoke调用方法的代码传递给调用的任何参数类型,都是方法将具有的参数类型。由于我们可以invoke多次调用,传递不同数量和类型的参数,这也意味着该invoke方法本质上有许多不同的类型。事实上,该invoke方法可以采用任何可通过普通 Java 方法声明声明的类型。因此签名是:多态的。返回类型也是如此。无论我们将方法的返回值转换为什么类型invoke,它都是方法将具有的返回类型。

调用该java.lang.reflect.Method::invoke方法需要创建一个新的Object[],将所有参数值存储到其中,这需要我们对原始值进行装箱。Object[]然后将Object传递给调用,如果该值是原始值,则必须再次对返回值进行拆箱。另一方面,签名多态性使我们能够避免所有这些开销。相反,所有参数值都按原样传递给 sig-poly 方法并从中返回。

此行为也在类文件中具体化。指向MethodHandle::invoke方法的类文件引用所附加的方法类型是参数值和返回值的确切(静态)类型,作为参数和返回类型。例如,如果我有一段这样的代码:

MethodHandle mh = ...
String x = ...
int y = ...
long result = (long) mh.invoke(x, y);

然后,为此次调用生成的invokevirtual指令将指向方法类型描述符。这与方法声明的方法类型描述符不同,后者是。javacinvoke(Ljava/lang/String;I)Jinvoke([Ljava/lang/Object;)Ljava/lang/Object;

我们可以使用javapJDK 自带的工具来检查javac这个调用点被分配了什么类型。使用 反汇编生成的类文件时javap -c ,我们可以找到一个带有类型描述符invokevirtual的方法指令:MethodHandle::invoke(Ljava/lang/String;I)J

46: invokevirtual #44  // Method java/lang/invoke/MethodHandle.invoke:(Ljava/lang/String;I)J

您可以在 JLS 中阅读有关此内容的更多信息,例如第15.12.3节

为了使 sig-poly 方法正常工作,VM 每次链接 sig-poly 方法时都会生成一些代码,并且对于 sig-poly 方法采用的每种类型,这种情况都会发生一次。此链接过程涉及旋转机器代码(通常是 Java 字节码),以使用正确的调用语义将方法句柄调用链接到实际目标方法(取决于目标方法,例如静态方法还是虚拟方法)。另一种思考方式是,sig-poly 方法可以有许多不同的实现。但是,这些MethodHandle实例实际上只是将调用转发到实例描述的实际目标方法的小型蹦床。

sig-poly 方法可以按原样接受参数,而无需将它们装入Object[],这是使用方法句柄的另一个很好的理由。如果您想要一个表示一段可调用代码的类型,但不确定参数的确切类型或数量,那么方法句柄是一个不错的选择。它们提供了一个通用的调用 API,而没有与装入参数和返回值相关的低效率。

5.处理接收器

接收器是this虚拟方法的参数。java.lang.reflect.Method将接收器作为类型的显式前导参数处理Object:

Object invoke(Object obj, Object... args)

调用static方法时,obj参数会被忽略。通常我们只是传递参数null来代替obj参数。

另一方面,对于方法句柄,接收方参数只是添加到参数列表的另一个参数。因此,如果我们查找一个虚拟方法

public static void main(String[] args) throws Throwable {
    MethodHandle targetMh = MethodHandles.lookup().findVirtual(Main.class, "target",
            MethodType.methodType(void.class));

    targetMh.invoke(new Main()); // prints 'invoking target'
}

public void target() { // NOT static
    System.out.println("invoking target");
}

调用方法句柄时,我们需要传递接收器的实例 ( new Main())。但是,当我们查找静态方法句柄时(如“什么是”中的第一个示例MethodHandle),没有额外的前导参数。

请特别注意用于查找虚方法的方法类型:

MethodType.methodType(void.class)

用于查找虚拟方法的方法类型不包含接收器类型。但是,接收器类型确实出现在返回的方法句柄的类型中。查找虚拟方法时,接收器类型由持有者类 (Main.class 在本例中) 暗示。查找虚拟方法时,这是需要牢记的重要事项。

6.invokeExact​方法

因为对 sig-poly 方法的调用会按原样传递参数,所以为调用生成的字节码invoke不会自动将类型为long的参数转换为int目标方法参数的类型。但是,invoke方法的实现会自动为我们处理这个问题。调用站点使用的invoke方法类型不必与方法句柄的类型匹配。invoke方法的实现会将所有参数和返回值转换为目标方法期望的类型。

另一方面,invokeExact方法不执行任何自动参数转换。相反,invokeExact的实现将检查调用站点使用的类型是否与方法句柄实例的类型完全匹配。如果不匹配,则抛出WrongMethodTypeException 。当我说类型需要完全匹配时,我的意思是完全匹配。如果方法句柄实例具有Object类型的参数,并且我传递静态类型为String的值,我将得到一个WrongMethodTypeException。为了在调用站点获取正确的类型,我需要将参数值转换为对象。

static void foo(Object o) {}

public static void main(String[] ___) throws Throwable {
    MethodHandle fooMh = MethodHandles.lookup().findStatic(Main.class, "foo",
            MethodType.methodType(void.class, Object.class));
    
    fooMh.invokeExact("Hello, world!");
}

此代码抛出:

java.lang.invoke.WrongMethodTypeException: handle's method type (Object)void but found (String)void

为了使调用成功,我必须将参数转换为Object,以便调用站点具有与方法句柄实例完全相同的类型:

fooMh.invokeExact((Object) "Hello, world!");

返回值也是如此。如果方法句柄实例的返回类型不是void,那么在使用invokeExact时,我们必须将返回值转换为正确的类型,即使没有使用,也可能需要分配该值:

static int foo() { return 42; }

public static void main(String[] ___) throws Throwable {
    MethodHandle fooMh = MethodHandles.lookup().findStatic(Main.class, "foo",
            MethodType.methodType(int.class));
    
    int x = (int) fooMh.invokeExact(); // result is not used
}

那么,为什么我们要使用invokeExact而不是invoke呢?因为事实证明,自动将参数转换为正确类型的成本很高。

invoke的实现将调用MethodHandle::asType方法。asType是我们的第一个方法句柄连接符示例。该asType方法接受MethodType并返回具有给定类型的新方法句柄,该方法执行所有必要的类型适配,然后将转换后的参数转发给原始方法句柄。如果需要,我们也可以手动调用:asType

static void foo(Object o) {}

public static void main(String[] ___) throws Throwable {
    MethodHandle fooMh = MethodHandles.lookup().findStatic(Main.class, "foo",
            MethodType.methodType(void.class, Object.class));

    // from (Object) -> void to (String) -> void
    fooMh = fooMh.asType(MethodType.methodType(void.class, String.class));

    fooMh.invokeExact("Hello, world!"); // this now works
}

为了实现这一点,asType的实现将生成一个合成类 + 方法,用于实现所有参数和返回类型转换。然后将结果包装在一个新的方法句柄中,并返回。就好像实现生成了asType一个这样的小包装器方法:

static void foo(String str) {
    foo((Object) str);
}

如果我们调用invoke方法句柄,实现将调用asType将方法句柄的类型转换为调用站点请求的类型。当我们只想调用一个方法时,很容易看出生成合成类 + 方法的成本有多高。但这并非全是坏事:每个方法句柄实例都有一个 1 元素缓存,并且的结果存储在该缓存中。如果下一次调用再次asType请求asType相同的方法类型,则只会返回缓存的方法句柄。实际上,这意味着只有在使用invoke不同的调用站点类型多次调用相同的方法句柄实例时,调用才会很昂贵,因为这会导致重复的缓存未命中,并使用请求的类型重新创建方法句柄。

虽然非常方便,但invoke的自动类型转换也可能是一个性能陷阱。但是,我们可以100% 确保通过使用invokeExact来避免这个陷阱,因为它永远不会执行任何类型适配。因此,作为性能经验法则:避免不精确的调用。最好使用invokeExact而不是invoke。

使用方法句柄时,通过调用invokeExact创建一个static包装函数通常很有用:

static final MethodHandle FOO_MH = ...

public static void main(String[] ___) throws Throwable {
    fooWrapper("Hello, world!");
}

public static void fooWrapper(Object o) {
    FOO_MH.invokeExact(o);
}

这里的fooWrapper方法与FOO_MH方法句柄具有完全相同的方法类型。但是,通过将其包装在static方法中,我们为客户端提供了一个非 sig-poly 方法以供调用。这意味着例如main上面示例中的方法不必担心显式地强制转换字符串参数为Object,同时避免方法句柄和调用站点之间的任何类型不匹配。我们说fooWrapper “文明化”了尖锐而灵活的invokeExactAPI。

7.invokeWithArguments​方法

由于java.lang.reflect.Method::invoke接受一个可变参数数组Object,我们不仅可以传入一个参数列表(它javac会自动放入一个中)Object[],还可以手动创建一个Object[]保存参数值的函数,然后java.lang.reflect.Method::invoke直接将其传递给它:

Method m = ...
m.invoke(null, 1, 2, 3); // 1.) Ok
Object[] args = { 1, 2, 3 };
m.invoke(null, args); // 2.) also OK

但是这种技术不适用于 sig-poly 方法。因为 sig-poly 方法的类型是从调用站点派生的,所以如果我们在调用Object[]方法句柄时传递一个,那么调用站点的类型将只具有Object[]参数类型。这个数组不会自动扩展为参数列表,因为参数再次按原样传递给 sig-poly 方法。

因此,如果我们希望将一个Object[]或一个List参数值传递给方法句柄,我们需要使用invokeWithArguments。此方法将数组或列表扩展为一系列标量参数值,然后将其传递给方法句柄。

static void foo(int x, int y, int z) {}

public static void main(String[] __) throws Throwable {
    MethodHandle fooMh = MethodHandles.lookup().findStatic(Main.class, "foo",
            MethodType.methodType(void.class, int.class, int.class, int.class));
    
    fooMh.invoke(1, 2, 3); // OK
    Object[] args = { 1, 2, 3 };
    // fooMh.invoke(args); // BAD
    // WrongMethodTypeException: cannot convert MethodHandle(int,int,int)void to (Object[])void
    fooMh.invokeWithArguments(args); // Ok
}

但请记住,这invokeWithArguments在内部也会进行类型调整,并且类似的性能警告也适用于MethodHandle::invoke。

8.方法句柄内联

现在我将讨论 JIT C2 编译器应用于方法句柄调用的内联优化。这是一个棘手的话题,但至少要了解一些,以便能够有效地使用方法句柄。方法句柄的不同用途之间可能存在很大的性能差异,大多数情况下,原因要么是调用不精确(请参阅上文invokeExact)要么是缺乏方法句柄内联。

对于常规的 Java 方法调用,我们知道调用指向哪个目标方法:

public static void foo() {}

public static void main(String[] args) {
    foo();
}

在上面的代码中,对foo的引用直接嵌入到类文件中。这意味着 JIT 编译器确切地知道正在调用哪个方法,并且可以内联该方法,从而实现其他优化。

然而,对于方法句柄,描述目标方法的信息嵌入在方法句柄实例中。因此,当调用方法句柄时,我们会通过一个 trampoline,从方法句柄实例读取目标方法,然后将调用转发到该目标。这种间接方式通常会阻止内联目标方法。毕竟,方法句柄调用的接收者可能是任意方法句柄实例,它可以指向任意目标方法。

但是,如果方法句柄是常量,则 JIT 编译器可以在 JIT 编译时通过检查常量方法句柄实例来确定目标方法,并将对它的调用视为对目标方法的正常调用,从而重新启用内联。

确定某个值(例如 a)是否为MethodHandle常量的最简单方法是简单地确定该值对于同一代码的不同调用是否会有所不同。

static final MethodHandle MH_FOO;

static {
    try {
        MH_FOO = MethodHandles.lookup().findStatic(Main.class, "foo",
                MethodType.methodType(void.class));
    } catch (ReflectiveOperationException e) {
        throw new ExceptionInInitializerError(e);
    }
}

static void foo() {}

static void m() throws Throwable {
    MH_FOO.invokeExact();
}

这里,invokeExact调用的接收者直接从字段加载。 static final和MH_FOO static final字段无法更改,甚至无法通过反射更改。 因此,JIT 可以将字段的加载常量折叠,使MH_FOO字段的接收者invokeExact成为常量。 然后,JIT 可以检查接收者的目标方法,并将对invokeExact的调用视为对foo目标方法的调用。

我们可以使用我在另一篇关于调试 JIT 编译器的文章第 3 章中描述的技术来检查invokeExact调用的内联是否正在进行: 3. 打印内联痕迹

如果我们应用这些技术来获取上述程序中m方法的内联跟踪,我们将得到以下内联跟踪:

@ 3   java.lang.invoke.LambdaForm$MH/0x00000236d7001400::invokeExact_MT (22 bytes)   force inline by annotation
  @ 10   java.lang.invoke.Invokers::checkExactType (17 bytes)   force inline by annotation
    @ 1   java.lang.invoke.MethodHandle::type (5 bytes)   accessor
  @ 14   java.lang.invoke.Invokers::checkCustomized (23 bytes)   force inline by annotation
    @ 1   java.lang.invoke.MethodHandleImpl::isCompileConstant (2 bytes)   (intrinsic)
  @ 18   java.lang.invoke.LambdaForm$DMH/0x00000236d7001800::invokeStatic (20 bytes)   force inline by annotation
    @ 8   java.lang.invoke.DirectMethodHandle::internalMemberName (8 bytes)   force inline by annotation
    @ 16   Main::foo (1 bytes)   inline (hot)

这里我们可以看到该方法Main::foo已被内联。

现在来看一个更复杂的例子:

...

static void m1() throws Throwable {
    m2(MH_FOO);
}

static void m2(MethodHandle mh) throws Throwable {
    mh.invokeExact();
}

这里我们调用的接收器实例invokeExact不是m2方法内部的常量。毕竟,m2传递给的参数可以是任意方法句柄实例。如果m2 是 JIT 编译,则无法invokeExact进行调用的内联。

然而,在m1方法中,我们将常量MH_FOO作为参数传递给m2。因此,如果该m1方法是 JIT 编译的,并且在该编译中m2是内联的,则的接收者invokeExact 将是一个常量,并且invokeExact可以进行调用的内联。

最后,让我们看一下实例字段:

class Widget {
    final MethodHandle mh_foo;

    ...

    void invoke() throws Throwable {
        mh_foo.invokeExact();
    }
}

这里,mh_foo是Widget实例的一个字段。如果invoke是 JIT 编译的,我们将从实例中获取一个负载this来加载mh_foo的值。由于this实例可以是Widget的任何实例,因此此负载不能进行常量折叠。因此, invokeExact的接收器也不是常量,并且我们没有得到内联。

static final Widget W = new Widget(MH_FOO);

static void m() throws Throwable {
    W.invoke();
}

在m方法,invoke的接收者是一个常量,因为它是从static final字段W加载的。因此,如果m是 JIT 编译的,并且invoke是内联的,则实例this将是一个常量。但是,在这种情况下,mh_foo的Widget字段的加载仍然不能进行常量折叠,因为final字段不是“可信的”。例如,它们仍然可以通过反射进行修改。因此,JIT 可能不会对字段mh_foo的加载进行常量折叠,invokeExact的接收者将不是常量,并且调用不能内联。

我们可以在内联跟踪中以MethodHandle::invokeBasic内联失败的调用形式看到这一点:

@ 18   java.lang.invoke.MethodHandle::invokeBasic()V (0 bytes)   failed to inline: receiver not constant

不过,这条规则也有一些例外。java.base某些包中的类具有受信任的final字段。此外,记录的字段也是受信任的。如果封闭实例也是常量,则 JIT 编译器可能会将这些字段常量折叠。因此,如果我们将Widget变成一条记录:

record Widget(MethodHandle mh_foo) {
    void invoke() throws Throwable {
        mh_foo.invokeExact();
    }
}

如果我们调用invoke一个常量Widget实例,则 JIT 将被允许折叠mh_foo字段的负载,并且invokeExact可以进行调用的内联。

9.方法句柄连接符

方法句柄连接符是方法句柄 API 的重要组成部分。其中大多数在MethodHandles类中以静态方法的形式出现,但有些MethodHandle也在类本身中以实例方法的形式出现。

方法句柄连接符是一种代码生成 API。每个方法句柄实例都包含一个名为LambdaForm的小程序,该程序描述了方法句柄的作用。在大多数情况下,这个LambdaForm会被渲染为字节码,然后直接执行。方法句柄连接符 API 可用于创建这些小程序,并将其包装在MethodHandle实例中。

前面提到过,MethodHandle::asType就是这样一个连接符。让我们再看一些其他值得注意的例子。

9.1.MethodHandles::insertArguments连接符

MethodHandles::insertArguments可能是您通常使用的最简单的连接符之一。它允许客户端创建一个新的方法句柄,将一个或多个固定参数值传递给给定的方法句柄:

static void foo(int x, int y, int z) {
    System.out.println(STR."x=\{x} y=\{y} z=\{z}");
}

public static void main(String[] __) throws Throwable {
    MethodHandle fooMh = MethodHandles.lookup().findStatic(Main.class, "foo",
        MethodType.methodType(void.class, int.class, int.class, int.class));
    
    // at parameter index 1, insert argument value '2'
    MethodHandle mh = MethodHandles.insertArguments(fooMh, 1, 2);

    mh.invokeExact(1, 3); // prints 'x=1 y=2 z=3'
}

在这里,我们在下游方法句柄的参数列表中的索引1处插入固定参数值2。结果是一个只接受 2 个参数的新方法句柄fooMh。这相当于foo像这样编写一个包装器方法:

static void fooPrime(int x, int z) {
    foo(x, 2, z);
}

9.2.MethodHandles::filterArguments连接符

接下来,MethodHandles::filterArguments创建一个方法句柄,通过调用过滤方法预处理其一个或多个参数,然后将结果传递给目标方法句柄:

static void foo(int x, int y, int z) {
    System.out.println(STR."x=\{x} y=\{y} z=\{z}");
}

static int bar(int x) {
    return x + 1;
}

public static void main(String[] __) throws Throwable {
    MethodHandle fooMh = MethodHandles.lookup().findStatic(Main.class, "foo",
        MethodType.methodType(void.class, int.class, int.class, int.class));
    MethodHandle barMh = MethodHandles.lookup().findStatic(Main.class, "bar",
        MethodType.methodType(int.class, int.class));
    
    // applies filter 'barMh' to argument at index 1, and passes the result to 'fooMh'
    MethodHandle mh = MethodHandles.filterArguments(fooMh, 1, barMh);

    mh.invokeExact(1, 2, 3); // prints 'x=1 y=3 y=3'
}

这相当于下面的java代码:

static void fooPrime(int x, int y, int z) {
    foo(x, bar(y), z);
}

值得注意的是,所使用的过滤器filterArguments必须恰好有 1 个参数,并且返回下游方法句柄在过滤索引处接受的类型的值。

9.3.MethodHandles::collectArguments连接符

MethodHandles::collectArguments限制较少,因为它允许将多个参数值压缩为一个值传递给下游方法句柄:

static void foo(int x, int y, int z) {
    System.out.println(STR."x=\{x} y=\{y} z=\{z}");
}

static int bar(int x, int y) {
    return x + y;
}

public static void main(String[] __) throws Throwable {
    MethodHandle fooMh = MethodHandles.lookup().findStatic(Main.class, "foo",
        MethodType.methodType(void.class, int.class, int.class, int.class));
    MethodHandle barMh = MethodHandles.lookup().findStatic(Main.class, "bar",
        MethodType.methodType(int.class, int.class, int.class));
    
    // applies filter 'barMh' to argument at index 1, and passes the result to 'fooMh'
    MethodHandle mh = MethodHandles.collectArguments(fooMh, 1, barMh);

    mh.invokeExact(1, 2, 2, 3); // prints 'x=1 y=4 y=3'
}

这相当于下面的java代码:

static void fooPrime(int a0, int a1, int a2, int a3) {
    foo(a0, bar(a1, a2), a3);
}

或者,收集器也可以接受零个参数,在这种情况下它可以像供应商一样动态生成参数值:

static void foo(int x, int y, int z) {
    System.out.println(STR."x=\{x} y=\{y} z=\{z}");
}

static int bar() {
    return ThreadLocalRandom.current().nextInt();
}

public static void main(String[] __) throws Throwable {
    MethodHandle fooMh = MethodHandles.lookup().findStatic(Main.class, "foo",
        MethodType.methodType(void.class, int.class, int.class, int.class));
    MethodHandle barMh = MethodHandles.lookup().findStatic(Main.class, "bar",
        MethodType.methodType(int.class));
    
    // applies filter 'barMh' to argument at index 1, and passes the result to 'fooMh'
    MethodHandle mh = MethodHandles.collectArguments(fooMh, 1, barMh);

    mh.invokeExact(1, 3); // prints 'x=1 y=<random numer> y=3'
}

这仍然非常简单,但collectArguments也可以与返回的收集器一起使用void。如果我们这样做,收集器本质上充当在下游方法句柄之前执行的副作用,而不是根据零个或多个输入计算下游句柄的参数。

对于void-returning 收集器,我们指定的索引collectArguments 表示下游句柄的参数列表中我们想要插入收集器参数列表的位置。这个索引不是指向下游句柄的特定参数,而是可以认为它指向参数之间的空间。其中,例如索引将指示0收集器的参数应出现在下游句柄的第一个参数之前。例如:

static void foo(String s, Object o, int i) {
}

static void bar(long l, double d) {
}

public static void main(String[] __) throws Throwable {
    MethodHandle fooMh = MethodHandles.lookup().findStatic(Main.class, "foo",
        MethodType.methodType(void.class, String.class, Object.class, int.class));
    MethodHandle barMh = MethodHandles.lookup().findStatic(Main.class, "bar",
        MethodType.methodType(void.class, long.class, double.class));

    MethodHandle mh = MethodHandles.collectArguments(fooMh, 0, barMh);
    System.out.println(mh.type()); // prints '(long,double,String,Object,int)void'
}

mh在这里的类型是(long,double,String,Object,int)void。

当然,我们甚至可以有一个不接受任何参数并且不返回任何内容的收集器。 collectArguments可能是最灵活的连接符。

9.4.MethodHandles::permuteArguments连接符

MethodHandles::permuteArguments可用于改变方法句柄的参数的顺序,或复制某些参数值并将它们“广播”给下游方法句柄的多个参数:

static void foo(int a0, int a1, int a2, int a3) {
    System.out.println(STR."a0=\{a0} a1=\{a1} a2=\{a2} a3=\{a3}");
}

public static void main(String[] __) throws Throwable {
    MethodHandle fooMh = MethodHandles.lookup().findStatic(Main.class, "foo",
        MethodType.methodType(void.class, int.class, int.class, int.class, int.class));

    MethodType newType = MethodType.methodType(void.class, int.class, int.class, int.class);
    int[] reorder = { 1, 0, 2, 2 };
    MethodHandle mh = MethodHandles.permuteArguments(fooMh, newType, reorder);

    mh.invokeExact(1, 2, 3); // prints 'a0=2 a1=1 a2=3 a3=3'
}

这相当于下面的java代码:

static void fooPrime(int a0, int a1, int a2) {
    foo(a1, a0, a2, a2);
}

permuteArguments接受一个描述概念包装方法类型的方法类型和一个“重新排序数组”,它描述了新类型中的每个参数如何连接到目标方法句柄的参数(fooMh)。

根据重新排序数组: fooMh的第一个参数应该接收索引处的参数1,fooMh 的第二个参数应该接收索引处的参数0, fooMh的第三和第四个参数都应该接收索引 2 处的参数。

permuteArguments的参数复制功能本质上允许您在下游方法句柄中多次使用参数值。它甚至可以用于删除下游方法句柄不需要的参数值,方法是指定一个比下游句柄具有更多参数的新方法类型,以及不使用每个参数索引的重新排序数组:

static void foo(int a0, int a1) {
    System.out.println(STR."a0=\{a0} a1=\{a1}");
}

public static void main(String[] __) throws Throwable {
    MethodHandle fooMh = MethodHandles.lookup().findStatic(Main.class, "foo",
        MethodType.methodType(void.class, int.class, int.class));

    MethodType newType = MethodType.methodType(void.class, int.class, int.class, int.class);
    int[] reorder = { 0, 1 };
    MethodHandle mh = MethodHandles.permuteArguments(fooMh, newType, reorder);

    mh.invokeExact(1, 2, 3); // prints 'a0=1 a1=2'
}

然而,对于该用例来说,MethodHandles::dropArguments连接符可能更方便。

9.5.其他连接符

MethodHandles和MethodHandle类中还有更多的连接符,但我不会在这里全部介绍。我介绍了一些常用的连接符,以给出连接符工作原理和推理的基本要点,但最终,学习所有连接符的最佳方法是亲自尝试它们。

正如我们所见,方法句柄连接符可以非常轻松地生成小段代码。连接符本质上是一种以编程方式编写 Java 代码的 API。连接符是您想要使用方法句柄的另一个很好的理由。

附录 A:MutableCallSite

MutableCallSite是包中另一个值得注意的类java.lang.invoke。它本质上只是方法句柄的持有者target。然而,特殊之处在于,即使该target字段是可变的,JIT 也可以从该字段中常量折叠负载(只要封闭MutableCallSite实例也是常量)。

这意味着您实际上可以拥有一个“基本不变”的方法句柄,它仍然可以利用方法句柄内联优化,但也可以将其换成另一个方法句柄。为了实现这一点,JIT 将在编译后的代码中记录对可变调用站点状态的“依赖性”,并且当调用站点的目标发生更改时,编译后的代码将被丢弃。这是一个强大的工具,但应谨慎使用,因为代码将返回到解释器中运行,并且必须由 JIT 再次编译。

附录 B:VarHandle

可以将 Var 句柄视为与内存访问特别相关的方法句柄的集合。它们不是一种单一invoke方法,而是具有各种get方法或set方法,这些方法使用不同的内存排序语义来实现内存访问。

变量句柄和方法句柄的性能注意事项相同。get*/set调用VarHandle 的接收方实例必须是常量,并且调用必须准确。但请注意,虽然方法句柄有专用invokeExact方法,但变量句柄没有。必须通过调用VarHandle::withInvokeExactBehavior明确启用变量句柄的确切调用行为 。此方法返回一个新的变量句柄,该句柄将get/set*检查调用站点类型是否与相应变量句柄的访问模式类型匹配。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值