11.7 方法句柄

Java 7 引入了全新的内省和方法访问机制。这种机制原本是为动态语言设计的,运行时可能需要加入方法调度决策机制。为了在 JVM 层支持这个机制,Java 引入了一个新字节码——invokedynamic。Java 7 并没有使用这个字节码,在 Java 8 中才大量用于 lambda 表达式和 Nashorn JavaScript 引擎中。

即便没有 invokedynamic,新方法句柄 API 的功能在很多方面也和反射 API 差不多,而且用起来更简洁,提出的概念更简单,就算单独使用也没问题。方法句柄可以理解成安全且现代化的反射。

11.7.1 MethodType对象

在反射 API 中,方法签名使用 Class[] 表示,这样处理起来很麻烦。而方法句柄 API 则使用 MethodType 对象表示。使用这种方式表示方法的类型签名更安全,而且更符合面向对象思想。

MethodType 对象包含返回值的类型和参数类型,但没有接收者的类型或方法的名称。因为没有方法的名称,所以具有正确签名的方法可以绑定到任何名称上(参照 lambda 表达式的函数式接口行为)。

方法的类型签名通过工厂方法 MethodType.methodType() 获取,是 MethodType 类的实例,而且不可变。例如:

  1. MethodType m2Str = MethodType.methodType(String.class); // toString()
  2. // Integer.parseInt()
  3. MethodType mtParseInt =
  4. MethodType.methodType(Integer.class, String.class);
  5. // ClassLoader类中的defineClass()方法
  6. MethodType mtdefClz = MethodType.methodType(Class.class, String.class,
  7. byte[].class, int.class,
  8. int.class);

虽然获取 MethodType 对象的方式看起来让人困惑,但获得的效果比反射 API 好,因为表示和讨论方法签名都更容易。下一步是通过一个查找过程,获取方法的句柄。

11.7.2 方法查找

方法查找查询在定义方法的类中执行,而且结果取决于执行查询的上下文。从下述示例可以看出,在一般的上下文中试图查找受保护的 Class::defineClass() 方法会失败,抛出 IllegalAccessException 异常,因为无法访问这个受保护的方法:

  1. public static void lookupDefineClass(Lookup l) {
  2. MethodType mt = MethodType.methodType(Class.class, String.class,
  3. byte[].class, int.class,
  4. int.class);
  5. try {
  6. MethodHandle mh =
  7. l.findVirtual(ClassLoader.class, "defineClass", mt);
  8. System.out.println(mh);
  9. } catch (NoSuchMethodException | IllegalAccessException e) {
  10. e.printStackTrace();
  11. }
  12. }
  13. Lookup l = MethodHandles.lookup();
  14. lookupDefineClass(l);

我们一定要调用 MethodHandles.lookup() 方法,基于当前执行的方法获取上下文 Lookup 对象。

Lookup 对象上可以调用几个方法(方法名都以 find 开头),查找需要的方法,包括 findVirtual()findConstructor()findStatic()

反射 API 和方法句柄 API 之间一个重大的区别是处理访问控制的方式。Lookup 对象只会返回在创建这个对象的上下文中可以访问的方法——没有任何方式能破坏这个规则(不像反射 API 可以使用 setAccessible() 方法调整访问控制)。

因此,方法句柄始终会遵守安全规则,而使用反射 API 的等效代码可能做不到这一点。方法句柄会在构建查找上下文时检查访问权限,所以不会为没有正确访问权限的方法创建句柄。

Lookup 对象或从中获取的方法句柄,可以返回给其他上下文,包括不再能访问该方法的上下文。在这种情况下,句柄依然是可以执行的,因为访问控制在查询时检查。这一点从下面的示例可以看出:

  1. public class SneakyLoader extends ClassLoader {
  2. public SneakyLoader() {
  3. super(SneakyLoader.class.getClassLoader());
  4. }
  5. public Lookup getLookup() {
  6. return MethodHandles.lookup();
  7. }
  8. }
  9. SneakyLoader snLdr = new SneakyLoader();
  10. l = snLdr.getLookup();
  11. lookupDefineClass(l);

通过 Lookup 对象可以为任何能访问的方法生成方法句柄,还能访问方法无法访问的字段。在 Lookup 对象上调用 findGetter()findSetter() 方法,分别可以生成读取字段和更新字段的方法句柄。

11.7.3 调用方法句柄

方法句柄表示调用方法的能力。方法句柄对象是强类型的,会尽量保证类型安全。方法句柄都是 java.lang.invoke.MethodHandle 类的子类实例,JVM 会使用特殊的方式处理这个类。

调用方法句柄有两种方式——使用 invoke() 方法或 invokeExact() 方法。这两个方法的参数都是接收者和调用参数。invokeExact() 方法尝试直接调用方法句柄,而 invoke() 方法在需要时会修改调用参数。

一般来说,invoke() 方法会调用 asType() 方法转换参数。转换的规则如下。

  • 如果需要,打包基本类型的参数。

  • 如果需要,拆包打包好的基本类型参数。

  • 如果需要,放大转换基本类型的参数。

  • 会把 void 返回类型修改为 0 或 null,具体是哪个取决于期待的返回值是基本类型还是引用类型。

  • 不管静态类型是什么,都能传入 null

考虑到可能会执行这些转换,所以要像下面这样调用方法句柄:

  1. Object rcvr = "a";
  2. try {
  3. MethodType mt = MethodType.methodType(int.class);
  4. MethodHandles.Lookup l = MethodHandles.lookup();
  5. MethodHandle mh = l.findVirtual(rcvr.getClass(), "hashCode", mt);
  6. int ret;
  7. try {
  8. ret = (int)mh.invoke(rcvr);
  9. System.out.println(ret);
  10. } catch (Throwable t) {
  11. t.printStackTrace();
  12. }
  13. } catch (IllegalArgumentException |
  14. NoSuchMethodException | SecurityException e) {
  15. e.printStackTrace();
  16. } catch (IllegalAccessException x) {
  17. x.printStackTrace();
  18. }

方法句柄提供的动态编程功能和反射一样,但处理方式更清晰明了。而且,方法句柄能在 JVM 的低层执行模型中很好地运转,因此,性能比反射好得多。