Method Handle

762 阅读30分钟

1、方法句柄的类型

对于一个方法句柄来说,它的类型完全由它的参数类型和返回值类型来确定,而与它所引用的底层方法的名称和所在的类没有关系。比如引用String类的length方法和Integer类的intValue方法的方法句柄的类型就是一样的,因为这两个方法都没有参数,而且返回值类型都是int。

在得到一个方法句柄,即java.lang.invoke.MethodHandle类的对象之后,可以通过其MethodHandle#type()方法来查看其类型,该方法的返回值是一个java.lang.invoke.MethodType对象。MethodType类的所有对象实例都是不可变的,类似于String类。所有对MethodType类型对象的修改,都会产生一个新的MethodType类对象。两个MethodType类对象是否相等,只取决于它们所包含的参数值类型和返回值类型是否完全一致。

1.1、创建方法句柄

MethodType类的对象实例只能通过MethodType类中的静态工厂方法来创建,这样的工厂方法有三类:

  1. 通过静态工厂方法MethodType#methodType()来创建。

    MethodType#methodType()方法通过指定参数和返回值的类型来创建MethodType,这主要是使用MethodType#methodType()方法的多种重载形式。

    • 使用这些方法的时候,至少需要指定返回值类型,而参数类型则可以是0到多个。

    • 返回值类型总是出现在MethodType#methodType()方法参数列表的第一个,后面紧接着的是0到多个参数的类型。

    • 类型都是由Class类的对象来指定的。

    • 如果返回值的类型是void,可以用void.classjava.lang.Void.class类声明。

    示例:

    public void generateMethodTypes(){
        //string.length()
        MethodType mt1 = MethodType.methodType(int.class);
        //string.concat(String str)
        MethodType mt2 = MethodType.methodType(String.class,String.class);
        //string.getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)
        MethodType mt3 = MethodType.methodType(void.class,int.class,int.class,char[].class,int.class);
        //string.startsWith(String prefix, int toffset)
        MethodType mt4 = MethodType.methodType(boolean.class,String.class,int.class);
    }
    
  2. 通过静态工厂方法MethodType#genericMethodType()来创建。

    MethodType#genericMethodType()可以生成通用的MethodType类型,即返回值和所有参数的类型都是Object类型。

    MethodType#genericMethodType()有两种重载形式:

    1. 第一种形式只需要指明方法类型中包含的Object类型的参数个数即可。
    2. 第二种形式可以提供一个额外的参数来说明是否在参数列表的后面添加一个Object[]类型的参数。
    public void generateGenericMethodTypes(){
        MethodType mt1 = MethodType.genericMethodType(3);
        MethodType mt2 = MethodType.genericMethodType(2,true);
    }
    
  3. 通过静态工厂方法MethodType#fromMethodDescriptorString()来创建。

    MethodType#fromMethodDescriptorString()比较复杂,其复杂之处在于其是通过java类型在字节码中的表示形式作为创建MethodType的参数,需要开发人员对java字节码有所了解。

    例如:

    • 方法string.getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin),其类型的字节码表示形式是(II[CI)V

    • (Ljava/lang/String;)Ljava/lang/String;表示参数类型是String,返回值类型是String

    示例:

    public void generateMethodTypesFromDescriptor(){
        String descriptor = "(II[CI)V";
        MethodType mt1 = MethodType.fromMethodDescriptorString(descriptor, getClass().getClassLoader());
    }
    

1.2、更新方法句柄

在通过工厂方法创建出MethodType之后,可以进一步对其进行修改,其修改都是围绕参数和返回值类型展开。所有的修改更新操作都会返回一个新的MethodType对象。参数索引从0开始。

示例:

public void changeMethodTypes() {
    //(int,int)String
    MethodType mt = MethodType.methodType(String.class, int.class, int.class);
    //追加参数:(int,int,float)String
    mt = mt.appendParameterTypes(float.class);
    //插入参数:(int,double,long,int,float)String
    mt = mt.insertParameterTypes(1, double.class, long.class);
    //删除参数:(int,double,int,float)String
    mt = mt.dropParameterTypes(2, 3);
    //变更参数:(int,double,String,float)String
    mt = mt.changeParameterType(2, String.class);
    //变更返回值:(int,double,String,float)void
    mt = mt.changeReturnType(void.class);
}

除了上述几个精确修改参数和返回类型的方法之外,还提供了几个一次性对返回值和参数类型进行处理的方法:

  • wrap():将基本类型转换为引用类型。
  • unwrap():将引用类型转换为基本类型。
  • generic():将所有参数和返回值转换为Object类型。
  • erase():将所有参数和返回值是引用类型的转换为Object类型。

示例:

public void wrapAndGeneric(){
    MethodType mt = MethodType.methodType(Integer.class, int.class, double.class);
    //基本类型转引用类型:(Integer,Double)Integer
    MethodType wrap = mt.wrap();
    //引用类型转基本类型:(int,double)int
    MethodType unwrap = mt.unwrap();
    //全部变成Object类型:(Object,Object)Object
    MethodType generic = mt.generic();
    //只引用类型变为Object类型:(int,double)Object
    MethodType erase = mt.erase();
}

2、方法句柄的调用

在获取到了一个方法句柄之后,最直接的使用方法就是调用它所引用的底层方法。在这点上,方法句柄的使用类似于反射API中的Method类型,但是方法句柄在调用时所提供的灵活性是Method中的invoke方法做不能比的。

首先使用java.lang.invoke.MethodHandleslookup()方法通过方法句柄类型(MethodType)找到对应的方法句柄,然后调用其所引用的底层方法。

示例:

public void invokeExact() throws Throwable {
    //定义方法句柄类型
    MethodType methodType = MethodType.methodType(String.class,int.class,int.class);
    //查找方法句柄
    MethodHandles.Lookup lookup = MethodHandles.lookup();
    MethodHandle mh = lookup.findVirtual(String.class, "substring", methodType);
    //调用方法句柄
    String string = (String)mh.invokeExact("Hello World", 1, 3);
    System.out.println(string); // el
    //
}

java.lang.invoke.MethodHandle提供了三个调用方法句柄的方法,分别是invokeExact()invoke()invokeWithArguments()

  • invokeExact()

    invokeExact()方法与直接调用底层方法是完全一样的。invokeExact()方法的参数依次是作为方法接收者的对象和调用时候的实际参数列表。如上代码所示,先获取String类的substring方法的句柄,再通过invokeExact()方法来进行调用,这种调用就相当于直接调用"Hello World".substring(1,3)

    需要注意的是invokeExact()方法在调用时要求严格的类型匹配,方法的返回值类型也是在考虑的范围之内的。如上代码所示,方法句柄所引用的substring方法的返回值类型是String,因此在使用invokeExact()方法进行调用时,需要在前面加上强制类型转换,以声明返回值的类型。如果不强制类型转换会怎样呢?

    • 直接赋值给一个Object类型变量,在调用的时候会抛出异常,因为invokeExact()方法认为方法的返回值类型是Object。抛出的异常如下:

      java.lang.invoke.WrongMethodTypeException: expected (String,int,int)String but found (String,int,int)Object
      
    • 去掉类型转换,并且也不进行赋值操作。这种情况下也是错误的,会抛出异常,因为invokeExact()方法认为方法的返回值类型是void。抛出的异常如下:

      java.lang.invoke.WrongMethodTypeException: expected (String,int,int)String but found (String,int,int)void
      
  • invoke()

    如果说invokeExact()方法是严格模式,那么invoke()方法就是非严格模式。

  • invokeWithArguments()

3、参数长度可变的方法句柄

在方法句柄中,所引用的方法中包含有可变长度参数的情况,这是一种比较特殊的情况。可变长度的参数实质上就是一个数组。对于这种特殊的情况,方法句柄提供了相关的处理能力,主要是一些转换方法,允许在可变长度参数和数组类型参数之间相互转换,以方便开发人员根据需求选择最合适的调用语法。

方法句柄主要提供了四个方法:

asVarargsCollector()

asVarargsCollector()方法的作用是把原始的方法句柄中最后一个数组类型的参数转换成对应类型的可变长度参数。

示例:

public class Varargs {
    public void normalMethod(String arg1,int arg2,int[] arg3) {
        System.out.println(arg1); //hello
        System.out.println(arg2); //2
        StringJoiner joiner = new StringJoiner(",");
        for (int arg : arg3) {
            joiner.add(arg+"");
        }
        System.out.println(joiner.toString()); //3,4,5
    }

    public void asVarargsController() throws Throwable {
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        MethodType methodType = MethodType.methodType(void.class,String.class, int.class, int[].class);
        MethodHandle mh = lookup.findVirtual(Varargs.class, "normalMethod", methodType);
        mh = mh.asVarargsCollector(int[].class);
        mh.invoke(this, "hello", 2, 3, 4, 5);
    }
}

asCollector()

asCollector()方法作用于asVarargsCollector()方法类似,不同之处在于asCollector()方法只会把指定数量的参数收集到原始方法句柄所对应的底层方法的数组类型参数中,而不像asVarargsCollector()方法那样可以收集任意数量的参数。

示例:

public class Varargs {
    public void normalMethod(String arg1,int arg2,int[] arg3) {
        System.out.println(arg1); //hello
        System.out.println(arg2); //2
        StringJoiner joiner = new StringJoiner(",");
        for (int arg : arg3) {
            joiner.add(arg+"");
        }
        System.out.println(joiner.toString()); //3,4
    }

    public void asController() throws Throwable {
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        MethodType methodType = MethodType.methodType(void.class,String.class, int.class, int[].class);
        MethodHandle mh = lookup.findVirtual(Varargs.class, "normalMethod", methodType);
        mh = mh.asCollector(int[].class,2);
        mh.invoke(this, "hello", 2, 3, 4);
    }
}

asSpreader()

上面的两个方法把数组类型的参数转换为可变长度的参数,自然还有与之对应的执行反方向转换的方法。

asSpreader()方法可以把可变长度的参数转换成数组类型的参数,转换之后的新方法句柄在调用的税后使用数组作为参数,而数组中的元素会被按顺序分配给原始方法句柄中的各个参数。

示例:

public class Varargs {
	public void toBeSpreader(String arg1,int arg2,int arg3,int arg4){
        System.out.printf("arg1=%s,arg2=%d,arg3=%d,arg4=%d",arg1,arg2,arg3,arg4);
        // arg1=hello,arg2=1,arg3=2,arg4=3
    }

    public void asSpreader() throws Throwable {
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        MethodType methodType = MethodType.methodType(void.class,String.class, int.class, int.class, int.class);
        MethodHandle mh = lookup.findVirtual(Varargs.class, "toBeSpreader", methodType);
        mh = mh.asSpreader(int[].class,3);
        mh.invoke(this, "hello", new int[]{1,2,3});
    }
}

如上代码所示,只能传递三个数字参数,如果传递三个以上,如new int[]{1,2,3,4},将会报错:java.lang.IllegalArgumentException: array is not of length 3

asFixedArity()

asFixedArity()方法是把可变长度的参数的方法转换成参数长度不可变的参数,即变成了对应的数组类型。在调用方法句柄的时候,就只能使用数组来进行参数传递。

示例:

public class Varargs {
	public void toBeFixedArity(String arg1,int... args){
        StringJoiner joiner = new StringJoiner(",");
        for (int arg : args) {
            joiner.add(arg+"");
        }
        System.out.println(joiner.toString());
    }

    public void asFixedArity() throws Throwable {
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        MethodType methodType = MethodType.methodType(void.class,String.class, int[].class);
        MethodHandle mh = lookup.findVirtual(Varargs.class, "toBeFixedArity", methodType);
        mh = mh.asFixedArity();
        mh.invoke(this, "hello", new int[]{1,2,3,4,5});
    }
}

如上代码所示,句柄调用时传递的参数是数组类型。如果传递非组数类型:mh.invoke(this, "hello", 1,2,3,4,5);,将会报错java.lang.invoke.WrongMethodTypeException: cannot convert MethodHandle(InvokeTest,String,int[])void to (InvokeTest,String,int,int,int,int,int)void

4、参数绑定

在“方法句柄的调用”一节已经介绍过,如果方法句柄在调用的时候引用的底层方法不是静态的,调用的第一个参数应该是该方法的接收者。这个参数的值一般在调用时指定。也可以事先进行绑定。通过MethodHandle的bindTo()方法可以预先绑定底层方法的调用接收者,而在实际调用的时候,只需要传入实际参数即可,不需要再指定方法的接收者。

示例:没有进行绑定,调用时需要指定。

public void bindTo() throws Throwable {
    MethodType methodType = MethodType.methodType(int.class);
    MethodHandles.Lookup lookup = MethodHandles.lookup();
    MethodHandle mh = lookup.findVirtual(String.class, "length", methodType);
    Object len = mh.invoke( "hello"); //值为5
}

示例:进行了绑定,调用时不需要在指定。

Object len = mh.bindTo("hello world").invoke(); //值为11

这样做好处在于:预先绑定参数的方式允许开发人员只公开某个方法,而不公开该方法所在的对象。开发人员只需要找到对应的方法句柄,并把适合的对象绑定到方法句柄上,客户代码就可以只获取到方法本身,而不会知道包含此方法的对象。绑定之后的方法句柄本身就可以在任何地方直接运行了。

bindTo()方法不仅仅只可以绑定方法的接收者,还可以多次使用bindTo()方法来为其中的多个参数绑定值。

示例:

public void multiBindTo() throws Throwable {
    MethodType methodType = MethodType.methodType(int.class, String.class, int.class);
    MethodHandles.Lookup lookup = MethodHandles.lookup();
    MethodHandle mh = lookup.findVirtual(String.class, "indexOf", methodType);
    mh = mh.bindTo("Hello") // 绑定了接收者
           .bindTo("l"); // 绑定了第一个参数
    Object invoke = mh.invoke(2); // 只需要指定第二个参数
    System.out.println(invoke); // 值为2
}

注意:在使用bindTo()方法方法进行参数绑定的时候,只能对引用类型的参数进行绑定,不能对int和float这样的基本数据类型进行绑定,否则将报错java.lang.IllegalArgumentException: no leading reference parameter。如果想要对包含基本数据类型参数的方法句柄进行绑定,可以先使用wrap()方法吧方法类型中的基本数据类型转换成对应的包装类型,再通过方法句柄的asType()方法将其转换成新的句柄。转换之后的新句柄就可以通过bindTo()方法来进行绑定。

示例:

public void wrapBindTo() throws Throwable {
    MethodType methodType = MethodType.methodType(int.class, int.class, int.class);
    MethodHandles.Lookup lookup = MethodHandles.lookup();
    MethodHandle mh = lookup.findVirtual(String.class, "indexOf", methodType);
    mh = mh.asType(mh.type().wrap());
    mh = mh.bindTo("Hello") // 绑定了接收者
        .bindTo(111); // 绑定了第一个参数 aka 111 = 'o'
    Object invoke = mh.invoke(1); // 只需要指定第二个参数
    System.out.println(invoke); // 值为4
}

5、获取方法句柄

方法句柄的查找是通过java.lang.invoke.MethodHandles.Lookup类来完成的。在查找之前,需要先通过MethodHandles.lookup()方法获取到一个MethodHandles.Lookup类的对象。MethodHandles.Lookup类提供了一些根据不同条件进行查找的方法。

5.1、获取构造方法、实例方法、静态方法句柄

示例:

  1. 查找构造方法

    MethodType mt = MethodType.methodType(void.class, byte[].class);
    lookup.findConstructor(String.class, mt);
    
  2. 查找实例方法(不局限方法的访问权限,即Public或Private等)

    MethodType mt = MethodType.methodType(String.class, int.class, int.class);
    MethodHandles.lookup().findVirtual(String.class,"substring",mt);
    
  3. 查找静态方法(不局限方法的访问权限,即Public或Private等)

    MethodType mt = MethodType.methodType(String.class, String.class, Object[].class);
    lookup.findStatic(String.class,"format", mt);
    

5.2、获取虚方法

为虚方法生成一个早期绑定的方法句柄,它将绕过对接收器上重写方法的检查。

如果目标方法是虚方法,实际将会调用其上层类方法。如下示例所示,调用的实例是子类实例,但通过findSpecial查找的方法句柄会从其声明的最上层类开始查找调用。

示例:

首先定义一个个父类和其对应的子类,并实现toString()

public class Father {
    @Override
    public String toString(){
        return "I'm Father.";
    }
}

public class Son extends Father {
    @Override
    public String toString() {
        return "I'm Son.";
    }

    static MethodHandles.Lookup lookup(){
        return MethodHandles.lookup();
    }
}

调用测试:

public void lookupSpecial() throws Throwable {
    Son fs = new Son();
    MethodHandles.Lookup lookup = Son.lookup();
    MethodType mt = MethodType.methodType(String.class);
    //从Father-Son,调用的是Father的toString方法
    MethodHandle mh_super = lookup.findSpecial(Father.class, "toString", mt, Son.class);
    System.out.println((String)mh_super.invokeExact(fs)); // I'm Father.
    //从Son,调用的是Son的toString方法
    MethodHandle mh_this = lookup.findSpecial(Son.class, "toString", mt, Son.class);
    System.out.println((String)mh_this.invokeExact(fs)); // I'm Son.
    //从Object-Father-Son,调用的是Father的toString方法
    MethodHandle mh_duper = lookup.findSpecial(Object.class, "toString", mt, Son.class);
    System.out.println((String)mh_duper.invokeExact(fs)); // I'm Father.
}

5.3、获取实例域或静态域句柄

所谓实例域或静态域是指对象中的实例成员变量和静态成员变量,如下所示,name即是实例域,age即是静态域:

public static class Simple{
    public String name;
    protected static int age;
}

对域的处理也是通过相应的设置和获取域的值的方法句柄来完成。对于域的查找只需要提供域所在类的class对象、域名称以及域类型即可。

示例:

public void lookupFieldAccessor() throws Throwable {
    Simple simple = new Simple();

    MethodHandles.Lookup lookup = MethodHandles.lookup();
    //设置name
    MethodHandle mh_name = lookup.findSetter(Simple.class, "name", String.class);
    mh_name.invoke(simple,"夏明");
    //获取name
    mh_name = lookup.findGetter(Simple.class, "name", String.class);
    System.out.println(mh_name.invoke(simple));
    //获取age
    MethodHandle mh_age = lookup.findStaticSetter(Simple.class, "age", int.class);
    mh_age.invoke(20);
    //获取age
    mh_age = lookup.findStaticGetter(Simple.class, "age", int.class);
    System.out.println(mh_age.invoke());
}

注意:

  1. 无论是实例域或静态域,其访问权限不能是private,只能是public、protected、package。

  2. 查找域的方法都是以Getter/Setter结尾,看起来很像是调用的其对应域的Getter/Setter方法,但实际上,是直接对字段赋值和获取值。

  3. 调用实例域时,需要提供调用的接收者作为参数;而静态与就不需要。

5.4、通过反射API获取对应句柄

除了直接在某个类中进行查找之外,还可以通过反射API得到ConstructorMethodField等对象获取对应的方法句柄。

  • unreflectConstructor():通过构造Constructor对象获取方法句柄。

    示例:

    MethodHandles.Lookup lookup = MethodHandles.lookup();
    Constructor<String> constructor = String.class.getConstructor(byte[].class);
    lookup.unreflectConstructor(constructor);
    
  • unreflect():通过Method对象获取方法句柄。

    示例:

    MethodHandles.Lookup lookup = MethodHandles.lookup();
    Method indexOf = String.class.getMethod("indexOf", int.class);
    lookup.unreflect(indexOf);
    
  • unreflectSpecial():通过特殊Method对象获取方法句柄。

    对于私有方法,需要通过unreflectSpecial()来进行转换,同样也需要提供

    public static class Simple{
        private void privateMethod(){
        }
    
        static MethodHandles.Lookup lookup(){
            return MethodHandles.lookup();
        }
    }
    
    MethodHandles.Lookup lookup = Simple.lookup()
    Method privateMethod = Simple.class.getDeclaredMethod("privateMethod");
    lookup.unreflectSpecial(privateMethod, Simple.class);
    
  • unreflectSetter()/unreflectGetter():通过Field获取方法句柄。

    public static class Simple{
        public String name;
    }
    
    MethodHandles.Lookup lookup = MethodHandles.lookup();
    Field field = Simple.class.getField("name");
    lookup.unreflectSetter(field);
    lookup.unreflectGetter(field);
    

5.5、通过工厂方法创建方法句柄

除了通过在java类中进行查找来获取方法句柄外,还可以通过java.lang.invoke.MethodHandles中提供的一些静态工厂方法来创建一些通用的方法句柄。

  1. arrayElementGetter()/arrayElementSetter()

    arrayElementGetter()/arrayElementSetter()方法用来设置和获取数组中元素的值的方法句柄。

    示例:

    public void arrayHandles() throws Throwable {
        int[] array = new int[]{1,2,3,4,5};
        MethodHandle setter = MethodHandles.arrayElementSetter(int[].class);
        setter.invoke(array,3,6);
    
        MethodHandle getter = MethodHandles.arrayElementGetter(int[].class);
        int value = (int)getter.invoke(array, 3);
        System.out.println(value); //值为6
    }
    
  2. identity()

    identity()方法的作用是通过它所生成的方法句柄,在每次调用的时候,总是返回其输入参数的值。

    在使用identity()方法的时候只需要传入方法句柄的唯一参数的类型即可,该方法句柄的返回值类型和参数类型是相同的。

    public void identify() throws Throwable {
        MethodHandle mh = MethodHandles.identity(String.class);
        String value = (String)mh.invoke("Hello");
        System.out.println(value); //值为Hello
    }
    
  3. constant()

    constant()方法的作用是在生成的时候指定一个常量值,以后这个方法句柄被调用的时候,总是返回这个常量值。在调用的时候不需要提供任何参数。这个方法同提供了把一种常量值转换成方法句柄的方式。

    public void constant() throws Throwable {
        MethodHandle mh = MethodHandles.constant(String.class, "Hello");
        String value = (String)mh.invoke();
        System.out.println(value); //值为Hello
    }
    

6、方法句柄变换

dropArguments

dropArguments方法可以在一个方法句柄的参数中添加一些无用的参数,这些参数在实际调用时不会被使用。但是它们可以使变换之后的方法句柄的参数类型格式符合某些特殊需要的模式,这也是这种变换方式的主要应用场景。

如下示例所示:

原方法句柄:(II)Ljava/lang/String;

调用dropArguments方法之后方法句柄:(FLjava/lang/String;II)Ljava/lang/String

public void dropArguments() throws Throwable {
    MethodType type = MethodType.methodType(String.class,int.class,int.class);
    MethodHandles.Lookup lookup = MethodHandles.lookup();
    MethodHandle mh = lookup.findVirtual(String.class, "substring", type);
    Object result = mh.invoke("Hello", 1, 5);
    System.out.println(result); //值为ello
    //变换方法句柄...
    //原方法句柄:
    //	MethodType.methodType(String.class,int.class,int.class);
    //在原方法句柄的第0个参数位增加float.class,String.class
    //新方法句柄:
    //	MethodType.methodType(float.class,String.class,String.class,int.class,int.class);
    MethodHandle methodHandle = MethodHandles.dropArguments(mh, 0, float.class,String.class);
    //在调用时只需要匹配模式,不会被真正调用,其结果还是一致的
    result = methodHandle.invoke(0.5f, "ignore","Hello", 1, 5);
    System.out.println(result);//值为ello
}

insertArguments

insertArguments方法与之前提到的MethodHandle#bindTo方法类似,这个方法可以同时为方法句柄中的多个参数预先绑定值,在得到的新的方法句柄中,已经绑定了具体值的参数不需要再提供,也不会出现在参数列表中。

示例:

public void insertArguments() throws Throwable {
    MethodType type = MethodType.methodType(String.class,String.class);
    MethodHandles.Lookup lookup = MethodHandles.lookup();
    MethodHandle mh = lookup.findVirtual(String.class, "concat", type);
    //绑定第一个参数值
    mh = MethodHandles.insertArguments(mh,1," World");
    //调用时已经绑定的参数就不用传了
    Object result = mh.invoke("Hello");
    System.out.println(result); //值为Hello World
}

filterArguments

filterArguments方法的作用是用于对方法句柄调用时的参数进行预处理,再把预处理的结果作为实际调用时的参数。预处理的过程是通过其他方法句柄来完成的,可以对一个或多个参数指定用来进行预处理的方法句柄。

示例:

public void filterArguments() throws Throwable {
    MethodHandles.Lookup lookup = MethodHandles.lookup();
    MethodHandle length = lookup.findVirtual(String.class, "length", 
                                             MethodType.methodType(int.class));
    MethodHandle max = lookup.findStatic(Math.class, "max", 
                                  MethodType.methodType(int.class, int.class, int.class));
    //从第0个参数开始预处理,处理完成之后交由max执行
    MethodHandle mh = MethodHandles.filterArguments(max, 0, length, length);
    Object result = mh.invoke("Hello","Hello World");
    System.out.println(result); //值为11
}

在使用filterArguments的时候,需要注意是的,filterArguments的第二个参数是要和后面的可变长度方法句柄参数配合起来使用的。第二个参数代表的是进行预处理的方法句柄所要进行处理参数在参数列表中的起始位置,紧跟在后面的是一系列对应的完成参数预处理的方法句柄。方法句柄与它要处理的参数是一一对应的,如果要跳过某个参数,只需要将null作为对应预处理方法句柄的值。在进行预处理时,需要注意原始方法句柄与预处理方法句柄的类型匹配,即预处理方法句柄的参数与要处理的参数类型匹配,并只能是一个参数;同样,其预处理方法句柄的返回只类型要与原始方法句柄的参数类型一致。

foldArguments

foldArguments方法的作用与filterArguments类似,都是用来对参数进行预处理。

不同之处在于:

  1. foldArguments方法对参数进行预处理之后的结果,不是替换掉原始的参数值,而是添加到原始参数列表的前面,作为一个新的参数。如果参数预处理的返回值是void,则不会添加新的参数。

  2. foldArguments方法参数预处理由一个方法句柄完成,不是像filterArguments方法由多个方法句柄完成。

    进行参数预处理的方法句柄会根据其类型中的参数个数N,从实际调用的参数列表中获取前N个参数作为它需要预处理的参数。如果预处理的方法句柄有返回值,则返回值的类型需要与原始方法句柄的第一个参数类型匹配,这是因为返回值会被作为调用原始方法句柄的第一个参数。

public static class FoldArguments{
    public static int testMethod(int arg1,int arg2,int arg3){
        System.out.println(arg1 + "--" + arg2 + "--" + arg3); // 值为4--3--4
        return arg1;
    }
    public void foldArguments() throws Throwable {
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        MethodHandle combiner = lookup.findStatic(Math.class, "max",
                     MethodType.methodType(int.class, int.class, int.class));
        MethodHandle target = lookup.findStatic(FoldArguments.class, "testMethod",
                     MethodType.methodType(int.class, int.class, int.class, int.class));
        MethodHandle mh = MethodHandles.foldArguments(target, combiner);
        Object result = mh.invoke(3, 4);
        System.out.println(result); //值为4
    }
}

另一点需要注意的是,调用foldArguments方法之后的句柄类型需要与原始方法句柄的类型匹配。

如上示例所示,预处理方法句柄的类型为(int,int)int,当调用foldArguments方法之后的句柄类型为(int,int,int)int,而这个方法句柄的类型需要与原始方法句柄的类型匹配,在上面的代码中,类型是匹配的,这是因为maxtestMethod都是静态方法的句柄。如果testMethod是实例方法,那么它的句柄类型就为(FoldArguments,int,int,int)int,如此类型就不匹配了。

permuteArguments

permuteArguments的作用是对调用时的参数顺序进行重新排列。再传递给原始方法句柄来调用。这种排列既可以是真正意义上全排列,即所有的参数都在重新排列之后的顺序出现;也可以是仅出现部分参数,没有出现的参数将被忽略;还可以重复某些参数,让这些参数在实际调用中出现多次。

示例:

public void permuteArguments() throws Throwable {
    MethodHandles.Lookup lookup = MethodHandles.lookup();
    MethodType mt = MethodType.methodType(int.class, int.class, int.class);
    MethodHandle compare = lookup.findStatic(Integer.class, "compare", mt);
    //未排序调用
    Object result = compare.invoke(3, 4);
    System.out.println(result); //值为-1
    //第一个和第二个参数替换调用,实际变为4,3
    compare = MethodHandles.permuteArguments(compare, mt, 1, 0);
    result = compare.invoke(3, 4);
    System.out.println(result); //值为1
    //第二个参数重新调用,实际变为4,4
    compare = MethodHandles.permuteArguments(compare, mt, 1, 1);
    result = compare.invoke(3, 4);
    System.out.println(result); //值为0
}

permuteArguments方法参数,第二个参数表示重新排列完成之后新方法句柄的类型,紧接着的是多个用来表示新的排列顺序的整数,这些整数的个数需与原始方法句柄的参数个数相同,整数出现的位置及其值就表示了在排列顺序上的对应关系。

catchException

catchException方法与原始方法句柄调用时的异常处理有关。可以通过该方法为原始方法句柄指定处理特定异常的方法句柄。如果原始方法句柄调用正常完成,则返回其结果;如果调用出现了特定异常,则异常处理方法句柄会被调用。通过该方法可以实现通用的异常处理逻辑,可以对程序中可能出现的异常都提供一个进行处理的方法句柄,再通过catchException方法来封装原始句柄。

示例:

public class CatchException{
    public int handlerException(Exception e,String str){
        System.out.println(e); //java.lang.NumberFormatException: For input string: "hello"
        System.out.println(str); //hello
        return -1;
    }
    public void catchException() throws Throwable {
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        //原始方法句柄
        MethodHandle parseInt = lookup.findStatic(Integer.class, "parseInt",
                          MethodType.methodType(int.class, String.class));
        //异常处理方法句柄
        MethodHandle handlerException = lookup.findVirtual(CatchException.class, "handlerException",MethodType.methodType(int.class, Exception.class, String.class)).bindTo(this);
        //创建异常处理句柄
        MethodHandle methodHandle = MethodHandles.catchException(parseInt, NumberFormatException.class, handlerException);

        Object result = methodHandle.invoke("hello");
        System.out.println(result); //-1
    }
}

注意事项:

  1. 原始方法句柄和异常方法句柄的返回值类型必须一致。因为当产生异常时,异常方法句柄的返回值将作为调用结果。
  2. 异常方法句柄的第一个参数是它所处理的异常类型,其他参数与原始方法句柄相同。
  3. 在异常方法句柄被调用时,其对应的底层方法可以得到原始方法句柄调用时的实际参数值。
  4. 在获得异常处理方法句柄时,使用了bindTo方法。这是因为通过findVirtual方法找到的方法句柄的第一个参数类型表示方法调用的接收者,这与catchException方法要求第一个参数必须是异常类型不符,因此通过bindTo方法为第一个参数预先绑定值。如果异常处理方法句柄所引用的是静态方法,就无此问题。

guardWithTest

guardWithTest实现了在方法句柄这个层次上的条件判断语义,相当于if-else。使用guardWithTest方法需要提供三个方法句柄,第一个是用来进行条件判断的,剩下两个则分别在条件成立和不成立时调用。用来进行条件判断的方法句柄其返回值类型必须是布尔型,剩下两个方法句柄返回类型必须一致。

示例:

public class GuardWithTest{
    public static boolean guardTest(){
        return Math.random() > 0.5;
    }
    public void guardWithTest() throws Throwable {
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        //条件方法句柄
        MethodHandle condition = lookup.findStatic(CatchException.class, "guardTest",
                                                   MethodType.methodType(boolean.class));

        MethodType mt = MethodType.methodType(int.class, int.class, int.class);
        MethodHandle max = lookup.findStatic(Math.class, "max",mt);
        MethodHandle mix = lookup.findStatic(Math.class, "min",mt);
        //if-else
        MethodHandle mh = MethodHandles.guardWithTest(condition, max, mix);

        Object result = mh.invoke(3,5);
        System.out.println(result); //随机3或5
    }
}

filterReturnValue

前面介绍的方法都是对方法句柄的参数进行处理的。filterReturnValue方法是用于对方法句柄被调用后的返回值进行处理。原始方法句柄被调用之后的结果会被传递给另外一个方法句柄进行再次处理,处理之后的结果被返回给调用者。

示例:

public void filterReturnValue() throws Throwable {
    MethodHandles.Lookup lookup = MethodHandles.lookup();
    MethodHandle substring = lookup.findVirtual(String.class, "substring",
                     MethodType.methodType(String.class, int.class));
    MethodHandle toUpperCase = lookup.findVirtual(String.class, "toUpperCase",
                     MethodType.methodType(String.class));
    //先交由substring处理,再将处理结果交由toUpperCase处理,最后返回
    MethodHandle mh = MethodHandles.filterReturnValue(substring, toUpperCase);
    Object result = mh.invoke("Hello World", 6);
    System.out.println(result); //WORLD
}

7、特殊方法句柄

在有些情况下,可能会需要对一组类型相同的方法句柄进行同样的变化操作。这个时候与其对所有的方法句柄都进行重复变换,不如创建出一个可以用来调用其他方法句柄的方法句柄。这种特殊的方法句柄的invoke方法或invokeExact方法被调用的时候,可以指定另外一个类型匹配的方法句柄作为实际调用的方法句柄。因为调用方法句柄时可以使用invoke和invokeExact两种方法,对应有两种创建这种特殊的方法句柄的方式,分别通过MethodHandles类的invoker和exactInvoker实现。两个方法都接受一个MethodType对象作为被调用的方法句柄的类型参数,两者的区别只在于调用时候的行为是类似于invoke还是invokeExact。

示例:调用方法句柄的方法句柄

public static class Invoker {
    public String substring(String str, int beginIndex, int endIndex) {
        System.out.println("substring");
        return str.substring(beginIndex, endIndex);
    }
    public void invoker() throws Throwable {
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        //创建invoker,即可以调用其他方法句柄的方法句柄
        MethodType methodType = MethodType.methodType(String.class, Object.class, int.class, int.class);
        MethodHandle invoker = MethodHandles.invoker(methodType);
        //创建被调用的方法句柄
        MethodType mt = MethodType.methodType(String.class, String.class, int.class, int.class);
        MethodHandle substring = lookup.findVirtual(Invoker.class, "substring", mt).bindTo(this);
        //调用其他方法句柄
        Object result = invoker.invoke(substring, "Hello", 1, 4);
        System.out.println(result); // ell
    }
}

示例:自动应用方法句柄变换操作

public static class Invoker {
    public String toUpperCase(String str) {
        System.out.println("toUpperCase");
        return str.toUpperCase();
    }
    public String substring(String str, int beginIndex, int endIndex) {
        System.out.println("substring");
        return str.substring(beginIndex, endIndex);
    }
    public void invoker() throws Throwable {
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        //创建invoker,即可以调用其他方法句柄的方法句柄
        MethodType methodType = MethodType.methodType(String.class, Object.class, int.class, int.class);
        MethodHandle invoker = MethodHandles.invoker(methodType);

        //方法句柄变换操作,返回还是invoker,当后续调用其他方法句柄时,此变换操作会自动被应用
        MethodHandle toUpperCase = lookup.findVirtual(Invoker.class, "toUpperCase",
                                                      MethodType.methodType(String.class, String.class)).bindTo(this);
        invoker = MethodHandles.filterReturnValue(invoker, toUpperCase);

        //创建被调用的方法句柄
        MethodType mt = MethodType.methodType(String.class, String.class, int.class, int.class);
        MethodHandle substring = lookup.findVirtual(Invoker.class, "substring", mt).bindTo(this);
        //调用其他方法句柄
        Object result = invoker.invoke(substring, "Hello", 1, 4);
        System.out.println(result); // ell
    }
}

8、使用方法句柄实现接口

JDK动态代理机制可以在运行时为多个接口动态创建实现类,并拦截通过接口进行的方法调用。方法句柄也具备动态实现一个接口的能力,这是通过java.lang.invoke.MethodHandleProxies类型中的静态方法asInterfaceInstance()来实现的。

通过方法句柄实现来实现接口所受限制比较多:

  1. 接口必须是public访问权限。

  2. 接口只能包含一个名称唯一的方法,即只能是函数式接口。

  3. 调用asInterfaceInstance()方法需要传递两个参数,

    1. 第一个参数是要实现的接口类的class对象。
    2. 第二个参数是处理方法调用逻辑的方法句柄对象。
  4. 方法的返回值是实现接口类的代理对象。

  5. 当调用方法时,这个调用会被代理给方法句柄来完成。

  6. 方法句柄的返回值作为接口调用的返回值。

  7. 接口方法的类型与方法句柄的类型必须兼容,否则会出现异常。

示例:

//被代理的接口
public interface Interface {
    Number number();
}

public class UseMethodHandleProxies {
    //方法句柄所对应的方法,其代理会被交由当前方法了完成
    public int doSomething(){
        System.out.println("doSomething...");
        return 1000;
    }
	
    public void methodHandleProxies() throws Throwable {
        //创建方法句柄
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        MethodType methodType = MethodType.methodType(int.class);
        MethodHandle mh = lookup.findVirtual(UseMethodHandleProxies.class, "doSomething", methodType);
        //绑定方法句柄接收者,这里必须提前绑定
        mh = mh.bindTo(this);
        //创建代理对象
        Interface instance = MethodHandleProxies.asInterfaceInstance(Interface.class, mh);
        System.out.println(instance.number());
    }
}

输出结果

class com.sun.proxy.jdk.proxy1.$Proxy0
doSomething...
1000

如上代码所示,Interface是一个代理对象,执行这个代理对象的number()方法的时候,实际上是交由方法句柄doSomething执行的。

初次看起来有些费解,如果你对JDK动态代理的实现有所了解的话,那么它的原理很简单。其asInterfaceInstance()方法实现的核心源码如下

@CallerSensitive
public static <T> T asInterfaceInstance(final Class<T> intfc, final MethodHandle target) {
	if (intfc.isInterface() && Modifier.isPublic(intfc.getModifiers())) {
		......
		final InvocationHandler ih = new InvocationHandler() {
        	......

			public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                for(int i = 0; i < methods.length; ++i) {
                    if (method.equals(methods[i])) {
                        return vaTargets[i].invokeExact(args);
                    }
                }

                if (method.getDeclaringClass() == WrapperInstance.class) {
                    return this.getArg(method.getName());
                } else if (MethodHandleProxies.isObjectMethod(method)) {
                    return MethodHandleProxies.callObjectMethod(proxy, method, args);
                } else if (MethodHandleProxies.isDefaultMethod(method)) {
                    return MethodHandleProxies.callDefaultMethod(defaultMethodMap, proxy, intfc, method, args);
                } else {
                    throw MethodHandleStatics.newInternalError("bad proxy method: " + method);
                }
            }
        };
        Object proxy;
        if (System.getSecurityManager() != null) {
            proxy = AccessController.doPrivileged(new PrivilegedAction<Object>() {
                public Object run() {
                    return Proxy.newProxyInstance(proxyLoader, new Class[]{intfc, WrapperInstance.class}, ih);
                }
            });
        } else {
            proxy = Proxy.newProxyInstance(proxyLoader, new Class[]{intfc, WrapperInstance.class}, ih);
        }

        return intfc.cast(proxy);
    } else {
            ......
    }
}

可以看到其中的关键对象以及方法InvocationHandlerProxy.newProxyInstance等。即其本质上也是通过JDK的动态代理来实现的,只不过其中的拦截对象InvocationHandler,不在是由我们自己定义,而是在asInterfaceInstance()方法中定义好了,由其转调asInterfaceInstance()方法第二个参数所声明的方法句柄。所以也就不难能理解,它为什么会比JDK动态代理实现有更多的限制。

所以,在使用时,asInterfaceInstance()方法第二个参数所声明的方法句柄在一定程度上等价与InvocationHandlerinvoke()方法。

通过方法句柄来实现接口的优势:

不需要新建额外的Java类,只需要复用已有的方法即可。

9、访问控制权限

在通过查找已有类中的方法得到方法句柄时,要受限于java语言中已有的访问控制权限,方法句柄与反射API在访问控制权限上的一个重要区别在于,在每次调用反射API的Method类型的invoke方法的时候都需要检查访问控制权限,而方法句柄只在查找的时候需要进行检查。只要在查找过程中不出现问题,方法句柄在使用中就不会出现与访问控制权限相关的问题。这种实现方法在使方法句柄在调用时的性能要优于Method类型。

通过MethodHandles.Lookup类的方法可以查找类中已有的方法以得到MethodHandle对象。而MethodHandles.Lookup类的对象本身则是通过MethodHandles类的静态方法lookup()得到的。在Lookup对象被创建的时候,会记录下当前所在的类(称为查找类)。只要查找类能够访问某个方法或域,就可以通过Lookup的方法来查找对应的方法句柄

示例:

public static class AccessControl {

    private void privateMethod(){
        System.out.println("privateMethod...");
    }

    public MethodHandle accessControl() throws Exception {
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        MethodType mt = MethodType.methodType(void.class);
        MethodHandle mh = lookup.findVirtual(AccessControl.class, "privateMethod", mt);
        return mh.bindTo(this);
    }
}

如上代码所示,privateMethod()为私有方法,位于AccessControl类中。而accessControl()方法也位于AccessControl类中,其获取privateMethod()的方法句柄就是在accessControl()方法中获取的。其他类可以通过这个方法句柄访问这个私有方法。虽然其他类虽然不能直接访问privateMethod()方法,但是在调用方法句柄的时候不会进行访问控制权限检查,因此对方法句柄的调用可以成功进行。

10、交换点

交换点是在多线程环境下控制方法句柄的一个开关。这个开关只有两个状态:有效和无效。交换点初始处于有效状态,一旦从有效状态变到无效状态,就无法再次改变状态。也就是说只允许发送一次状态改变。这种状态变化是全局和即时生效的。使用同一个交换点的多个线程会即时观察到状态的变化。交换点用java.lang.invoke.SwitchPoint类来表示。通过SwitchPoint对象的guardWithTest()方法可以设置在交换点的不同状态下调用不同的方法句柄。这个方法的作用类似于MethodHandlesguardWithTest()方法,只不过少了用来进行条件判断的方法句柄,只有条件成立和不成立时分别调用的方法句柄。这是因为选择那个方法句柄来执行是由交换点的有效状态来决定的,不需要额外的条件判断。

示例:

public void useSwitchPoint() throws Throwable {
    MethodHandles.Lookup lookup = MethodHandles.lookup();
    MethodType methodType = MethodType.methodType(int.class, int.class, int.class);
    MethodHandle max = lookup.findStatic(Math.class, "max", methodType);
    MethodHandle min = lookup.findStatic(Math.class, "min", methodType);
    //创建交换点
    SwitchPoint sp = new SwitchPoint();
    //设置不同交换点状态执行的方法句柄,并返回一个新的句柄
    MethodHandle methodHandle = sp.guardWithTest(min, max);
    System.out.println(methodHandle.invoke(3,4)); // 3
    //使交换点失效
    SwitchPoint.invalidateAll(new SwitchPoint[]{sp});
    System.out.println(methodHandle.invoke(3,4)); // 4
}

交换点的一个重要作用是在多线程环境下使用,可以在多个线程中共享同一个交换点对象。当某个线程的交换点状态改变之后,其他所使用guardWithTest()方法的线程返回的方法句柄的调用行为就会发生变化。

11、使用方法句柄进行函数式编程

方法句柄是一个非常灵活的对方法进行操作的轻量级结构。方法句柄的作用类似于在某些语言中出现的函数指针。在程序中,方法句柄可以在对象之间自由传递,不受访问控制的限制。方法句柄的这种特性,使得Java语言中也可以进行函数式编程。

示例:

数组作为一种常见的数据结构,有的编程语言提供了对它进行复杂操作的功能。这些功能中比较常见的是:

  • forEach:对数组中的每个元素都依次执行某个操作。
  • map:把原始数组按照一定的转换过程编程一个新的数组。
  • reduce:把一个数组按照某种规则编程单个元素。

forEach实现:

public static <T> void forEach(T[] array,MethodHandle handle) throws Throwable {
    for (int i = 0; i < array.length; i++) {
        handle.invoke(array[i],i);
    }
}

map实现:

@SuppressWarnings("unchecked")
public static <T,R> R[] map(T[] array,MethodHandle handle,Class<R> clazz) throws Throwable {
    R[] results = (R[])Array.newInstance(clazz, array.length);
    for (int i = 0; i < array.length; i++) {
        results[i] = (R)handle.invoke(array[i],i);
    }
    return results;
}

reduce实现:

@SuppressWarnings("unchecked")
public static <T,R> R reduce(T[] array,R initValue,MethodHandle handle) throws Throwable {
    R result = initValue;
    for (T t : array) {
        result = (R) handle.invoke(result, t);
    }
    return result;
}