理解 JDK 中的 MethodHandle

作者:梁德泉

Java是一个支持反射的语言,从诞生的那一刻起就已经支持了反射。经典的反射代码可以这样写:

public class Main {
    public static void main(String[] args) throws Throwable {
        Method m = Super.class.getMethod("x", boolean.class);
        m.invoke(new Super(), false); // super::primitive
        m.invoke(new Super(), Boolean.FALSE); // super::primitive
        m.invoke(new Sub(), false); // sub::primitive
        m.invoke(new Sub(), Boolean.FALSE); // sub::primitive

        m = Sub.class.getMethod("x", Boolean.class);
        // m.invoke(new Super(), false); // IllegalArgumentException: object is not an instance of declaring class
        m.invoke(new Sub(), false); // sub::boxed
        m.invoke(new Sub(), Boolean.FALSE); // sub::boxed

        // m.invoke(null, false); // NullPointerException
        // m.invoke(new Sub()); // IllegalArgumentException: wrong number of arguments
    }
}
class Super {
    public int x(boolean a) {System.out.println("super::primitive");return 1;}
    public int x(Boolean a) {System.out.println("super::boxed");return 1;}
}
class Sub extends Super {
    public int x(boolean a) {System.out.println("sub::primitive");return 1;}
    public int x(Boolean a) {System.out.println("sub::boxed");return 1;}
}

从上述代码可以看出,可以利用反射在运行时通过名字查找方法句柄,动态调用想要的方法。同时,上面的例子中也可以发现,通过Method反射调用是支持多态的。

一切完美。但是从Java7开始,JDK中又多了一个功能类似的成员,java.lang.invoke.MethodHandle。先来看看MethodHandle又是怎么用的:

public class MHTest {
    public static void main(String[] args) throws Throwable {
        MethodType mt = MethodType.methodType(int.class, Boolean.class);
        MethodHandle mh = MethodHandles.lookup().findVirtual(MHSuper.class, "x", mt);
        Object a;
//        mh.bindTo(null).invoke(false); // NullPointerException
//        mh.invoke(false); // WrongMethodTypeException: cannot convert MethodHandle(MHSuper,Boolean)int to (boolean)void
//        mh.bindTo(new Object()).invoke(false); // ClassCastException: Cannot cast java.lang.Object to MHSuper
        mh.bindTo(new MHSuper()).invoke(false); // super::boxed
        mh.bindTo(new MHSuper()).invoke(Boolean.FALSE); // super::boxed
        a = (int)mh.bindTo(new MHSuper()).invokeExact(Boolean.FALSE); // super::boxed
//        a = (Number)mh.bindTo(new MHSuper()).invokeExact(Boolean.FALSE); // WrongMethodTypeException: expected (Boolean)int but found (Boolean)Number
//        a = (Integer)mh.bindTo(new MHSuper()).invokeExact(Boolean.FALSE); // WrongMethodTypeException: expected (Boolean)int but found (Boolean)Integer
//        mh.bindTo(new MHSuper()).invokeExact(Boolean.FALSE); // WrongMethodTypeException: expected (Boolean)int but found (Boolean)void

        mh.bindTo(new MHSub()).invoke(false); // sub::boxed
        mh.bindTo(new MHSub()).invoke(Boolean.FALSE); // sub::boxed
        a = (int)mh.bindTo(new MHSub()).invokeExact(Boolean.FALSE); // sub::boxed

        mh = MethodHandles.lookup().findStatic(MHSuper.class, "y", mt);
        mh.invoke(false); // super::static
        a = (int)mh.invokeExact(Boolean.FALSE); // super::static

        mh = MethodHandles.lookup().findStatic(MHSuper.class, "z", MethodType.methodType(int.class, MHSuper.class));
        MHSuper sup = new MHSub();
        a = (int)mh.invokeExact(sup); // class MHSub
//        MHSub sub = new MHSub();a = (int)mh.invokeExact(sub); // WrongMethodTypeException: expected (MHSuper)int but found (MHSub)int
    }
}

class MHSuper {
    public static int y(Boolean a) {System.out.println("super::static");return 1;}
    public static int z(MHSuper a) {System.out.println(a.getClass());return 1;}
    public int x(boolean a) {System.out.println("super::primitive");return 1;}
    public int x(Boolean a) {System.out.println("super::boxed");return 1;}
}
class MHSub extends MHSuper {
    public static int y(Boolean a) {System.out.println("sub::static");return 1;}
    public int x(boolean a) {System.out.println("sub::primitive");return 1;}
    public int x(Boolean a) {System.out.println("sub::boxed");return 1;}
}

MethodHandle反射调用也是支持多态的,并且和Method不同的是,MethodHandle的成员方法要线bindTo到某个instance,bind过程中已经做了类型检查;而Method成员方法左值是和函数参数一起传入的。注意到MethodHandle比Method多了一个invokeExact方法,它与invoke的区别是方法参数、返回值匹配非常严格,调用时如果有数值参数隐式转换(如short转int/子类转父类)、装箱拆箱,会直接抛异常。

在Java源码层面/运行表现层面来看,两者区别并不大,那为什么会在JDK在新引入、MethodHandle这个特性呢?先从字节码看一看区别:

// Method
// method.invoke(new Super(), false);
invokevirtual #37                 // Method java/lang/reflect/Method.invoke:(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
// (int)method.invoke(new Super(), false);
invokevirtual #44                 // Method java/lang/reflect/Method.invoke:(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
// method.invoke(new Super(), Boolean.FALSE);
invokevirtual #51                 // Method java/lang/reflect/Method.invoke:(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;

// MethodHandle
// mh.bindTo(new MHSuper()).invoke(false);
invokevirtual #56                 // Method java/lang/invoke/MethodHandle.invoke:(Z)V
// mh.bindTo(new MHSuper()).invoke(Boolean.FALSE);
invokevirtual #77                 // Method java/lang/invoke/MethodHandle.invoke:(Ljava/lang/Boolean;)V
// (int)mh.bindTo(new MHSuper()).invoke(false);
invokevirtual #60                 // Method java/lang/invoke/MethodHandle.invoke:(Z)I
// (Integer)mh.bindTo(new MHSuper()).invoke(false);
invokevirtual #67                 // Method java/lang/invoke/MethodHandle.invoke:(Z)Ljava/lang/Integer;
// (Number)mh.bindTo(new MHSuper()).invoke(false);
invokevirtual #70                 // Method java/lang/invoke/MethodHandle.invoke:(Z)Ljava/lang/Number;

很容易发现,不管源码长什么样子,Method反射调用全部都是相同的签名;而MethodHandle则会根据源码形参类型生成不同的字节码(符号表),相当于在.class文件中携带了更多信息。容易想到,生成这些特化字节码需要编译器javac支持,而javac则是根据注解 @java.lang.invoke.MethodHandle.PolymorphicSignature 做代码生成。文档中描述如下:

In source code, a call to a signature polymorphic method will compile, regardless of the requested symbolic type descriptor. As usual, the Java compiler emits an invokevirtual instruction with the given symbolic type descriptor against the named method. The unusual part is that the symbolic type descriptor is derived from the actual argument and return types, not from the method declaration.

简单来说就是调用标记了PolymorphicSignature的方法时,不管源码传什么参数都是可以编译通过的,编译器其实不按源码中描述的方法签名生成字节码,而是参考实际传入参数的形式类型(或者称为变量类型更合适)生成。

在MethodHandle的文档描述中还有一点值得关注:MethodHandle的访问性检查只在创建时检查一次,而Method则是每次调用都检查。

Unlike with the Core Reflection API, where access is checked every time a reflective method is invoked, method handle access checking is performed when the method handle is created. In the case of ldc (see below), access checking is performed as part of linking the constant pool entry underlying the constant method handle.Thus, handles to non-public methods, or to methods in non-public classes, should generally be kept secret. They should not be passed to untrusted code unless their use from the untrusted code would be harmless.

也就是说,使用MethodHadle时更像平时写Java代码,只有public成员是可以访问的,protected、private的成员只能在类内部代码才能用MethodHandles工厂方法访问,外部是无法生成MethodHandle对象的(但自己创建然后泄漏出去就不怪JDK了)。而Method则是全局都能访问,比如我们惯用的技巧就是使用反射获取 sun.misc.Unsafe 引用。

综上,MethodHandle更像是在Java语法规则内手写字节码:自己创建方法签名(MethodType),自己决定调用方式(invokestatic/invokespecial/invokevirtual),自己注意访问控制(public/package/protected/private),最后还要自己决定类型隐式转换;而Method权限则大得多。虽然MethodHandle能力相对受限,不过性能确高了很多(两者的实现对比解析待续…)。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏杨熹的专栏

Day 2-Java-imooc-9-继承

课程地址:http://www.imooc.com/learn/124 总结图片来自 http://www.imooc.com/article/10715 ?...

2738
来自专栏Java帮帮-微信公众号-技术文章全总结

Java基础-17(01)总结,登录注册案例,Set集合,HashSet

1:登录注册案例(理解) 需求:用户登录注册案例。 按照如下的操作,可以让我们更符号面向对象思想 A:有哪些类呢? B:每个类有哪些东西呢? C:类与类之...

3817
来自专栏逆向与安全

从虚拟机角度看Java多态->(重写override)的实现原理

工具与环境: Windows 7 x64企业版 Cygwin x64 jdk1.8.0_162 openjdk-8u40-src-b25-10_feb_201...

1040
来自专栏云霄雨霁

Java--类和对象之初始化和清除

1075
来自专栏Phoenix的Android之旅

深入分析ClassCastException

ClassCastException时常见,只要两个不同类强转换就会有这种问题,不过下面这种错误不知道见过没

471
来自专栏黑泽君的专栏

基本布尔类型的Getter方法是isXxx

对于所有的基本数据类型,Getter方法名都必须叫GetXxx,Setter方法名都必须叫setXxx。

361
来自专栏AzMark

Python学习之面向对象「 中 」

822
来自专栏大前端_Web

javascript对象属性的赋值解析

版权声明:本文为吴孔云博客原创文章,转载请注明出处并带上链接,谢谢。 https://blog.csdn.net/wkyseo/articl...

1193
来自专栏程序员宝库

一些我认为有用有趣的 JDK 方法

在学习JDK的源码过程中我遇到了一些有趣有用的方法,在此之前如果要使用这些工具方法,我首先会想到的是 commons-lang和 guava这样的语言扩展包,但...

2717
来自专栏领域驱动设计DDD实战进阶

01-JavaScript之变量

这个系列的文章主要讲解JavaScript的常见用法,适合于初中级的前端开发人员,也可以对比TypeScript的系列文章来看。 先介绍JavaScript的变...

3217

扫码关注云+社区