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 类的实例,而且不可变。例如:
MethodType m2Str = MethodType.methodType(String.class); // toString()// Integer.parseInt()MethodType mtParseInt =MethodType.methodType(Integer.class, String.class);// ClassLoader类中的defineClass()方法MethodType mtdefClz = MethodType.methodType(Class.class, String.class,byte[].class, int.class,int.class);
虽然获取 MethodType 对象的方式看起来让人困惑,但获得的效果比反射 API 好,因为表示和讨论方法签名都更容易。下一步是通过一个查找过程,获取方法的句柄。
11.7.2 方法查找
方法查找查询在定义方法的类中执行,而且结果取决于执行查询的上下文。从下述示例可以看出,在一般的上下文中试图查找受保护的 Class::defineClass() 方法会失败,抛出 IllegalAccessException 异常,因为无法访问这个受保护的方法:
public static void lookupDefineClass(Lookup l) {MethodType mt = MethodType.methodType(Class.class, String.class,byte[].class, int.class,int.class);try {MethodHandle mh =l.findVirtual(ClassLoader.class, "defineClass", mt);System.out.println(mh);} catch (NoSuchMethodException | IllegalAccessException e) {e.printStackTrace();}}Lookup l = MethodHandles.lookup();lookupDefineClass(l);
我们一定要调用 MethodHandles.lookup() 方法,基于当前执行的方法获取上下文 Lookup 对象。
在 Lookup 对象上可以调用几个方法(方法名都以 find 开头),查找需要的方法,包括 findVirtual()、findConstructor() 和 findStatic()。
反射 API 和方法句柄 API 之间一个重大的区别是处理访问控制的方式。Lookup 对象只会返回在创建这个对象的上下文中可以访问的方法——没有任何方式能破坏这个规则(不像反射 API 可以使用 setAccessible() 方法调整访问控制)。
因此,方法句柄始终会遵守安全规则,而使用反射 API 的等效代码可能做不到这一点。方法句柄会在构建查找上下文时检查访问权限,所以不会为没有正确访问权限的方法创建句柄。
Lookup 对象或从中获取的方法句柄,可以返回给其他上下文,包括不再能访问该方法的上下文。在这种情况下,句柄依然是可以执行的,因为访问控制在查询时检查。这一点从下面的示例可以看出:
public class SneakyLoader extends ClassLoader {public SneakyLoader() {super(SneakyLoader.class.getClassLoader());}public Lookup getLookup() {return MethodHandles.lookup();}}SneakyLoader snLdr = new SneakyLoader();l = snLdr.getLookup();lookupDefineClass(l);
通过 Lookup 对象可以为任何能访问的方法生成方法句柄,还能访问方法无法访问的字段。在 Lookup 对象上调用 findGetter() 和 findSetter() 方法,分别可以生成读取字段和更新字段的方法句柄。
11.7.3 调用方法句柄
方法句柄表示调用方法的能力。方法句柄对象是强类型的,会尽量保证类型安全。方法句柄都是 java.lang.invoke.MethodHandle 类的子类实例,JVM 会使用特殊的方式处理这个类。
调用方法句柄有两种方式——使用 invoke() 方法或 invokeExact() 方法。这两个方法的参数都是接收者和调用参数。invokeExact() 方法尝试直接调用方法句柄,而 invoke() 方法在需要时会修改调用参数。
一般来说,invoke() 方法会调用 asType() 方法转换参数。转换的规则如下。
如果需要,打包基本类型的参数。
如果需要,拆包打包好的基本类型参数。
如果需要,放大转换基本类型的参数。
会把
void返回类型修改为 0 或null,具体是哪个取决于期待的返回值是基本类型还是引用类型。不管静态类型是什么,都能传入
null。
考虑到可能会执行这些转换,所以要像下面这样调用方法句柄:
Object rcvr = "a";try {MethodType mt = MethodType.methodType(int.class);MethodHandles.Lookup l = MethodHandles.lookup();MethodHandle mh = l.findVirtual(rcvr.getClass(), "hashCode", mt);int ret;try {ret = (int)mh.invoke(rcvr);System.out.println(ret);} catch (Throwable t) {t.printStackTrace();}} catch (IllegalArgumentException |NoSuchMethodException | SecurityException e) {e.printStackTrace();} catch (IllegalAccessException x) {x.printStackTrace();}
方法句柄提供的动态编程功能和反射一样,但处理方式更清晰明了。而且,方法句柄能在 JVM 的低层执行模型中很好地运转,因此,性能比反射好得多。
