Java优化 - 方法句柄

本文深入探讨Java7引入的Method Handles特性,展示了其如何提供比反射更强大的运行时方法调用灵活性,同时保持静态类型安全。通过代码示例,解释了Method Handles的查找和调用过程,以及其在性能和安全性上的优势。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Java优化 - 方法句柄

Java 7引入的invokedynamic,为确定调用侧(call site)执行哪个方法带来很大的灵活性-关键点是,到运行时才能确定。
当解释器执行到调用侧的时候,一个特定的辅助方法BSM(bootstrap method)被调用。BSM返回一个对象,它代表调用侧应该调用的实际方法。这叫调用目标,它被加进调用侧。
关键概念是方法句柄(Method Handles)-代表被调用侧调用的方法。它有时候类似反射,但是反射固定的限制导致不方便invokedynamic使用。
Java 7加了一些新类和包(特别是java.lang.invoke.MethodHandle),表示方法的直接的可执行引用。这些方法句柄对象有一组允许底层方法的执行的相关方法。其中,invoke()是最常用的,还有一些助手和轻微变体。
和反射调用一样,一个方法句柄的底层方法可以有任何签名。方法句柄也有一些反射不具备的特性。
要理解新特性是什么,和为什么重要,让我们先考虑一些简单的反射地调用一个方法的代码:

    Method m = ...
    Object receiver = ...
    Object o = m.invoke(receiver, new Object(), new Object());

对应的字节码如下:

17: iconst_0
18: new             #2 // class java/lang/Object
21: dup
22: invokespecial   #1 // Method java/lang/Object."<init>":()V
25: aastore
26: dup
27: iconst_1
28: new             #2 // class java/lang/Object
31: dup
32: invokespecial   #1 // Method java/lang/Object."<init>":()V
35: aastore
36: invokevirtual   #3 // Method java/lang/reflect/Method.invoke:(Ljava/lang/Object;[Ljava/lang/Object;)

iconst和aastore操作码被用来把可变参数的第0个和第1个元素保存进一个数组,传给invoke()方法。然后,调用的整体签名就清楚了:(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object,该方法接受一个对象参数(receiver),后面跟着一个可变参数,它们被传给反射的调用。它最终返回一个Object,所有的这些说明,在编译时不知道这个方法调用是什么,到了运行时才知道。
所以,这是个通用的调用,如果receiver和Method对象不匹配,或者参数列表不正确,可能会导致运行时故障。
我们对比一下,看看方法句柄的一个简单例子:

    MethodType mt = MethodType.methodType(int.class);
    MethodHandles.Lookup l = MethodHandles.lookup();
    MethodHandle mh = l.findVirtual(String.class, "hashCode", mt);
    
    String receiver = "b";
    int ret = (int) mh.invoke(receiver);
    System.out.println(ret);

代码可以分成两个部分:首先查找该方法句柄,然后调用它。在实际的系统里,这两个部分可以在时间上或者代码位置上都分得很远。方法句柄是不可变对象,可以缓存。
当一个类被初始加载的时候,会检查字节码。这包括确保类不会调用它无权访问的方法。如果试图调用无权调用的方法,会导致类加载过程故障。
考虑到性能,一旦类被加载了,就不会再做进一步的检查。所以,反射的代码可以利用它。
方法句柄API采取的方法不同:查找上下文。使用它,通过调用MethodHandles.lookup(),我们增加一个上下文对象。返回的不可变对象记录了哪些方法和成员是可访问的状态。
这意味着该上下文对象可以被立即使用,也可以先保存下来。这样的灵活性允许选择行地访问一个类的私有方法。而反射只能通过setAccessible(),破坏了Java访问控制的安全性。
我们看看方法句柄例子中查找段的字节码:

       0: getstatic     #12                 // Field java/lang/Integer.TYPE:Ljava/lang/Class;
       3: invokestatic  #13                 // Method java/lang/invoke/MethodType.methodType:(Ljava/lang/Class;)Ljava/lang/invoke/MethodType;
       6: astore_1
       7: invokestatic  #14                 // Method java/lang/invoke/MethodHandles.lookup:()Ljava/lang/invoke/MethodHandles$Lookup;
      10: astore_2
      11: aload_2
      12: ldc           #15                 // class java/lang/String
      14: ldc           #16                 // String hashCode
      16: aload_1
      17: invokevirtual #17                 // Method java/lang/invoke/MethodHandles$Lookup.findVirtual:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle;
      20: astore_3

该代码已经生成了一个上下文对象,它能看到lookup()静态调用发生之处可以访问的每个方法。从这里,我们能使用findVirtual()(和相关方法)得到该点可见的任何方法的一个句柄。如果我们试图访问一个查找上下文不可见的方法,会抛出IllegalAccessException/不像反射,程序员无法破坏或者关闭该检查。
我们的例子里,我们简单查找字符串的public hashCode()方法,它不需要特殊的访问权限。不过,我们必须仍然使用该查找机制,平台会检查上下文对象是否能访问请求的方法。接下来,让我们看看调用方法句柄的字节码:

      21: ldc           #18                 // String b
      23: astore        4
      25: aload_3
      26: aload         4
      28: invokevirtual #19                 // Method java/lang/invoke/MethodHandle.invoke:(Ljava/lang/String;)I
      31: istore        5
      33: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
      36: iload         5
      38: invokevirtual #20                 // Method java/io/PrintStream.println:(I)V

这里和反射有很大的不同,这是因为调用invoke()不是简单的访问任何参数的one-size-fits-all,而是运行时应该调用的期望的方法签名。
我们的例子里,调用签名是invoke:(Ljava/lang/String;)I,MethodHandle的JavaDoc里没有任何内容指明该类有这样的方法。
javac源代码编译器已经为这次调用推断了正确的类型签名,然后发射了调用,即使MethodHandle没有这样的方法。javac发射的字节码也设置堆栈,以便以常用的方法调用,而不用把可变参数装入数组。
对于开发者来说,可以认为方法句柄的功能和反射类似,不过尽可能地提供了静态类型安全性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值