浅析方法句柄

方法句柄

JKD 7 中引入了java.lang.invoke包,即方法句柄,是反射的轻量级实现,它的作用是间接调用方法 ,方法句柄中首先涉及到两个重要的类,MethodHandle和MethodType

1. MethodHandle

它是对最终调用方法的”引用”,类似于C++中的函数指针,或者说,它是一个有能力安全调用方法的对象。方法句柄类似于反射中的Method类,他们本质上都是模拟方法调用,但是Reflection是在模拟Java代码层次的方法调用,而MethodHandle是在模拟字节码层次的方法调用,在MethodHandles.Lookup上的3个方法findStatic(),findVirtual(),findSpecial()正是为了对应invokestatic,invokevirtual(以及invokeinterface)和invokespecial这几条字节码指令的执行权限校验行为。由于方法句柄是对字节码的方法指令调用的模拟,那理论上虚拟机在这方法做的各种优化(如方法内联),在MethodHandle上也应当可以采用类似思路去支持,因此方法句柄功能更强大灵活,MethodHandle可以服务于JVM上的所有语言

2. MethodType

它是表示方法签名类型的不可变对象。每个方法句柄都有一个MethodType实例,用来指明被调用方法的返回类型和参数类型。它的类型完全由返回类型和参数类型确定,而与它所引用的底层的方法的名称和所在的类没有关系。举个例子,String类的length()方法和Integer类的intValue()方法的方法句柄的类型就是一样的,因为这两个方法都没有参数,而返回值都是int,则我们可以通过下面语句获取同一个方法类型:MethodType mt = MethodType.methodType(int.class);
MethodType的对象实例只能通过MethodType类中的静态工厂方法来创建,而且MethodType类的所有对象实例都是不可变的,类似于String类。如果修改了MethodType实例中的信息,就会生成另外一个MethodType实例

3. 使用方法句柄的简单案例
public class MethodHandleTest {

    public MethodHandle getHandler() {
        MethodHandle mh = null;
        MethodType mt = MethodType.methodType(String.class, int.class, int.class);
        MethodHandles.Lookup lk = MethodHandles.lookup();

        try {
            mh = lk.findVirtual(String.class, "substring", mt);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return mh;
    }

    public static void main(String[] args) throws Throwable {
        MethodHandle mh = new MethodHandleTest().getHandler();
        String str = "Hello World!";

        Object result1 = mh.invoke(str, 1, 3);
        Object result2 = (String) mh.invokeExact(str, 1, 3);

        /**
         * 下面这行代码在运行时会报错,因为方法类型为String.class, int.class, int.class
         * 而下面这行代码的返回类型为Object,与申明中为String不符
         * 下面这行代码其中第二个参数类型为Integer,与声明中为int不符,则类型适配不符合,系统报错
         */
//        Object result3 = mh.invokeExact(str, new Integer(1), 3);

        System.out.println("result 1: " + result1);
        System.out.println("result 2: " + result2);
    }
    
}

上述代码的输出都是 el

4. 方法句柄的调用过程
  1. 先通过MethodType的静态工厂方法生成一个包含方法返回类型,方法参数类型的方法类型,也就是MethodType mt = MethodType.methodType(String.class, int.class, int.class)(这里假设调用方法是String类的substring(int, int)方法)
  2. 然后,获取方法句柄要用到的Lookup对象,例如上述代码中的 lk实例,这个对象可以提供其所在环境中任何可见方法的方法句柄。我们可以把他想象成包含某个类对象的方法成员,方法的容器,通过 lk.findVirtual(String.class, “substring”, mt); 具体锁定String类中的名字为”substring”,且方法类型为mt的方法,作为方法句柄返回,总而言之,要想从lookup对象中得到方法句柄,需要给出三个参数,分别为,持有所需方法的类,方法的名称,以及跟方法相匹配的方法类型
  3. 最后,获取到方法句柄之后,我们就可以通过方法句柄来调用底层的方法。方法句柄提供两个方法调用底层方法,invoke()和invokeExact()方法。
5. invoke()方法和invokeExact()方法
  1. 二者的相同点

二者的参数列表是相同的,第一个参数为方法的接收对象,即是哪个对象执行这个方法,接下来的参数就是执行方法所需要的参数

需要注意的是,参数列表中的第一个参数(接收对象)可以通过以下方式省略,可以通过方法句柄的bindTo()方法来绑定具体的接收对象,从而使得方法句柄的调用和普通方法没什么区别

  • 关于bindTo()方法的代码示例
public class MethodHandleTest {

    static class ClassA {
        public void println(String s) {
            System.out.println(s);
        }
    }

    public static MethodHandle getPrintMH(Object receiver) throws Throwable {
        MethodType mt = MethodType.methodType(void.class, String.class);
        return lookup().findVirtual(receiver.getClass(), "println", mt).bindTo(receiver);
    }

    public static void main(String[] args) throws Throwable {
        Object obj = System.currentTimeMillis() % 2 == 0 ? System.out : new ClassA();
        getPrintMH(obj).invokeExact("HelloWorld!");
    }

}
  • 程序输出结果总是 HelloWorld
  1. 二者的不同点

从名字上来看,明显是invokeExact方法准确性更高,或者说要求更严格,invokeExact方法调用时要求严格的类型匹配,方法的返回值类型也在考虑范围之内,如果上面注释掉的代码一样。如果把第二次调用中Object result2 = (String) mh.invokeExact(str, 1, 3); 中的强制类型转换去掉的话,在调用的时候方法会认为返回值为Object类型而不是String类型,然后系统报错

与invokeExact方法不同,invoke方法允许稍微松散的调用方式,它会尝试在调用的时候进行返回值类型和参数类型的转换工作,从而使得方法类型不完全相等或者返回值类型不同的方法调用会产生一个新的方法句柄,来适配此次方法调用,转换基本规则如下,假设方法句柄原MethodType为S,新的适配方法句柄MethodType为T

  1. 可以通过java的类型转换来完成,一般从子类转成父类,比如从String到Object类型;
  2. 可以通过基本类型的转换来完成,只能将类型范围的扩大,比如从int切换到long;
  3. 可以通过基本类型的自动装箱和拆箱机制来完成,例如从int到Integer;
  4. 如果S有返回值类型,而T的返回值类型为void,则S的返回值会被丢弃;
  5. 如果S的返回值是void,而T的返回值是引用类型,T的返回值会是null;
  6. 如果S的返回值是void,而T的返回值是基本类型,T的返回值会是0;
    前三点好理解,第4,5,6点我用一个例子说明一下
public class MethodHandleTest {
 
    public MethodHandle getHandler() {
        MethodHandle mh = null;
        MethodType mt = MethodType.methodType(void.class);
        MethodHandles.Lookup lk = MethodHandles.lookup();
         
        try {
            mh = lk.findVirtual(MethodHandleTest.class, "print", mt);
        } catch (Throwable e) {
            e.printStackTrace();
        }
         
        return mh;
    }
     
    public void print() {
        System.out.println("print");
    }
     
    public static void main(String[] args) throws Throwable {
        MethodHandleTest mht = new MethodHandleTest();
        MethodHandle mh = mht.getHandler();
         
        int result1 = (int) mh.invoke(mht);
        Object result2 = mh.invoke(mht);
         
        System.out.println("result 1:" + result1);
        System.out.println("result 2:" + result2);
    }
}
Tags: