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(
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值