java方法句柄-----1.方法句柄类型、调用

方法句柄

  方法句柄(method handle)是JSR 292中引入的一个重要概念,它是对Java中方法、构造方法和域的一个强类型的可执行的引用。这也是句柄这个词的含义所在。通过方法句柄可以直接调用该句柄所引用的底层方法。从作用上来说,方法句柄的作用类似于2.2节中提到的反射API中的Method类,但是方法句柄的功能更强大、使用更灵活、性能也更好。实际上,方法句柄和反射API也是可以协同使用的,下面会具体介绍。
在Java标准库中,方法句柄是由java.lang.invoke.MethodHandle类来表示的。

1.方法句柄的类型

  对于一个方法句柄来说,它的类型完全由它的参数类型和返回值类型来确定,而与它所引用的底层方法的名称和所在的类没有关系。比如引用String类的length方法和Integer类的intValue方法的方法句柄的类型就是一样的,因为这两个方法都没有参数,而且返回值类型都是int。
  在得到一个方法句柄,即MethodHandle类的对象之后,可以通过其type方法来查看其类型。该方法的返回值是一个java.lang.invoke.MethodType类的对象。MethodType类的所有对象实例都是不可变的,类似于String类。所有对MethodType类对象的修改,都会产生一个新的MethodType类对象。两个MethodType类对象是否相等,只取决于它们所包含的参数类型和返回值类型是否完全一致。

1.1MethodType类的对象实例的创建

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

1.1.1 通过指定参数和返回值的类型来创建MethodType.【显式地指定返回值和参数的类型】

  这主要是使用methodType方法的多种重载形式。使用这些方法的时候,至少需要指定返回值类型,而参数类型则可以是0到多个。
  返回值类型总是出现在methodType方法参数列表的第一个后面紧接着的是0到多个参数的类型。类型都是由Class类的对象来指定的。如果返回值类型是void,可以用void.class或java.lang.Void.class来声明。
  代码清单2-31中给出了使用methodType方法的几个示例。注意:最后一个methodType方法调用中使用了另外一个MethodType的参数类型作为当前MethodType类对象的参数类型

代码清单2-31 MethodType类中的methodType方法的使用示例

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)
MethodType mt4=MethodType.methodType(boolean.class, mt2);
}

1.1.2 通过静态工厂方法genericMethodType来创建的

  除了显式地指定返回值和参数的类型之外,还可以生成通用的MethodType类型,即返回值和所有参数的类型都是Object类
  方法genericMethodType有两种重载形式:
  第一种形式只需要指明方法类型中包含的Object类型的参数个数即可。
  第二种形式可以提供一个额外的参数来说明是否在参数列表的后面添加一个Object[]类型的参数。
  在代码清单2-32中,mt1有3个类型为Object的参数,而mt2有2个类型为Object的参数和后面的Object[]类型参数。

代码清单2-32 生成通用MethodType类型的示例

public void generateGenericMethodTypes(){
MethodType mt1=MethodType.genericMethodType(3);
MethodType mt2=MethodType.genericMethodType(2,true);
}

1.1.2 通过静态工厂方法fromMethodDescriptorString来创建的

  最后介绍的一个工厂方法是比较复杂的fromMethodDescriptorString。这个方法允许开发人员指定方法类型在字节代码中的表示形式作为创建MethodType时的参数。这个方法的复杂之处在于字节代码中的方法类型格式不是很好理解。
  比如代码清单2-31中的String.getChars方法的类型在字节代码中的表示形式是“(II[CI)V”。不过这种格式比逐个声明返回值和参数类型的做法更加简洁,适合于对Java字节代码格式比较熟悉的开发人员。
  在代码清单2-33中,“(Ljava/lang/String;)Ljava/lang/String;”所表示的方法类型是返回值和参数类型都是java.lang.String,相当于使用MethodType.methodType(String.class, String.class)。
代码清单2-33 使用方法类型在字节代码中的表示形式来创建MethodType

public void generateMethodTypesFromDescriptor(){
ClassLoader cl=this.getClass().getClassLoader();
String descriptor="(Ljava/lang/String;)Ljava/lang/String;";
MethodType mt1=MethodType.fromMethodDescriptorString(descriptor, cl);
}

注意:在使用fromMethodDescriptorString方法的时候,需要指定一个类加载器。该类加载器用来加载方法类型表达式中出现的Java类。如果不指定,默认使用系统类加载器。

2 对MethodType类的对象实例的修改

2.1 围绕返回值和参数类型的精确修改

  在通过工厂方法创建出MethodType类的对象实例之后,可以对其进行进一步修改。这些修改都围绕返回值和参数类型展开。所有这些修改方法都返回另外一个新的MethodType对象。

代码清单2-34 对MethodType中的返回值和参数类型进行修改的示例

public void changeMethodType(){
//(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);
}

2.2 一次性对返回值和所有参数的类型进行修改

  除了上面这几个精确修改返回值和参数的类型的方法之外,MethodType还有几个可以一次性对返回值和所有参数的类型进行处理的方法。
  代码清单2-35给出了这几个方法的使用示例,其中wrap和unwrap用来在基本类型及其包装类型之间进行转换generic方法把所有返回值和参数类型都变成Object类型,而erase只把引用类型变成Object,并不处理基本类型。修改之后的方法类型同样以注释的形式给出。

代码清单2-35 一次性修改MethodType中的返回值和所有参数的类型的示例

public void wrapAndGeneric(){
//(int, double)Integer
MethodType mt=MethodType.methodType(Integer.class, int.class, double.class);
//(Integer, Double)Integer
MethodType wrapped=mt.wrap();
//(int, double)int
MethodType unwrapped=mt.unwrap();
//(Object, Object)Object
MethodType generic=mt.generic();
//(int, double)Object
MethodType erased=mt.erase();
}

由于每个对MethodType对象进行修改的方法的返回值都是一个新的MethodType对象,可以很容易地通过方法级联来简化代码。

3.方法句柄的调用

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

3.1 通过invokeExact方法实现

  最直接的调用一个方法句柄的做法是通过invokeExact方法实现的。这个方法与直接调用底层方法是完全一样的。
  invokeExact方法参数依次是作为方法接收者的对象调用时候的实际参数列表
比如在代码清单2-36中,这种调用方式就相当于直接调用"Hello World".substring(1,3)
代码清单2-36 使用invokeExact方法调用方法句柄

public void invokeExact()throws Throwable{
// 1.先获取String类中substring的方法句柄.
MethodHandles.Lookup lookup=MethodHandles.lookup(); 
MethodType type=MethodType.methodType(String.class, int.class, int.class);
MethodHandle mh=lookup.findVirtual(String.class,"substring",type);
// 2.再通过invokeExact来进行调用。
String str=(String)mh.invokeExact("Hello World",1,3);
System.out.println(str);
}

  在这里强调一下静态方法和一般方法之间的区别静态方法在调用时不需要指定方法的接收对象的,而一般的方法则是需要的。如果方法句柄mh所引用的是java.lang.Math类中的静态方法min,那么直接通过mh.invokeExact(3,4)就可以调用该方法。
  注意:invokeExact方法在调用的时候要求严格的类型匹配,方法的返回值类型也是在考虑范围之内的。代码清单2-36中的方法句柄所引用的substring方法的返回值类型是String,因此在使用invokeExact方法进行调用时,需要在前面加上强制类型转换,以声明返回值的类型。
  如果去掉这个类型转换,而直接赋值给一个Object类型的变量,在调用的时候会抛出异常,因为invokeExact会认为方法的返回值类型是Object。如下图所示:
invokeExact方法调用后返回值类型的严格匹配
去掉类型转换但是不进行赋值操作也是错误的,因为invokeExact会认为方法的返回值类型是void,也不同于方法句柄要求的String类型的返回值。
invokeExact方法调用后返回值类型的严格匹配

3.1 通过invoke方法实现

  与invokeExact所要求的类型精确匹配不同的是,invoke方法允许更加松散的调用方式。它会尝试在调用的时候进行返回值和参数类型的转换工作。这是通过MethodHandle类的asType方法来完成的。asType方法的作用是把当前的方法句柄适配到新的MethodType上,并产生一个新的方法句柄。当方法句柄在调用时的类型与其声明的类型完全一致的时候,调用invoke等同于调用invokeExact;否则,invoke会先调用asType方法来尝试适配到调用时的类型。如果适配成功,调用可以继续;否则会抛出相关的异常。这种灵活的适配机制,使invoke方法成为在绝大多数情况下都应该使用的方法句柄调用方式。
  进行类型适配的基本规则是比对返回值类型和每个参数的类型是否都可以相互匹配。只要返回值类型或某个参数的类型无法完成匹配,那么整个适配过程就是失败的。从待转换的源类型S到目标类型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。
    满足上面规则时进行两个方法类型之间的转换是会成功的。

let's see how it's possible to use the invoke() with a boxed argument:

 @Test
    public void givenReplaceMethodHandle_whenInvoked_thenCorrectlyReplaced() throws Throwable {
        MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();
        MethodType mt = MethodType.methodType(String.class, char.class, char.class);
        MethodHandle replaceMH = publicLookup.findVirtual(String.class, "replace", mt);
        String replacedString = (String) replaceMH.invoke("jovo", Character.valueOf('o'), 'a');
        String replacedString3 = (String) replaceMH.invoke("jovo",  'o', 'a');
        String replacedString2 = (String) replaceMH.invoke("jovo",  new Character('o'), 'a');
        String replacedString4 = (String) replaceMH.invokeExact("jovo",  'o', 'a');
        String replacedString5 = (String) replaceMH.invokeExact("jovo",  new Character('o'), 'a'); //不能使用包装类,报错
        assertEquals("java", replacedString);
    }

In this case, the replaceMH requires char arguments, the invoke() performs an unboxing on the Character argument before its execution.通过MethodHandle类的asType方法尝试在调用的时候进行参数类型的转换工作
invokeExact方法调用后返回值类型的严格匹配

3.3 通过invokeWithArguments方法实现

  最后一种调用方式是使用invokeWithArguments。该方法在调用时可以指定任意多个Object类型的参数。完整的调用方式是首先根据传入的实际参数的个数.

    1. 通过MethodType的genericMethodType方法得到一个返回值和参数类型都是Object的新方法类型。
    1. 再把原始的方法句柄通过asType转换后得到一个新的方法句柄
    1. 最后通过新方法句柄的invokeExact方法来完成调用

这个方法相对于invokeExact和invoke的优势在于,它可以通过Java反射API被正常获取和调用,而invokeExact和invoke不可以这样。它可以作为反射API和方法句柄之间的桥梁。

MethodType mt = MethodType.methodType(List.class, Object[].class);

MethodHandle asList = publicLookup.findStatic(Arrays.class, "asList", mt);
 
List<Integer> list = (List<Integer>) asList.invokeWithArguments(1,2);
 
assertThat(Arrays.asList(1,2), is(list));

methodHandle类中的invokeWithArguments方法

    public Object invokeWithArguments(Object... arguments) throws Throwable {
        MethodType invocationType = MethodType.genericMethodType(arguments == null ? 0 : arguments.length);
        return invocationType.invokers().spreadInvoker(0).invokeExact(asType(invocationType), arguments);
    }

4.参数长度可变的方法句柄 --- 简化方法调用时的语法

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

4.1 MethodHandle的asVarargsCollector方法

  MethodHandle中第一个与长度可变参数相关的方法是asVarargsCollector。它的作用是把原始的方法句柄中的最后一个数组类型的参数转换成对应类型的可变长度参数
  如代码清单2-37所示,方法normalMethod的最后一个参数是int类型的数组,引用它的方法句柄在通过asVarargsCollector方法转换之后,得到的新方法句柄在调用时就可以使用长度可变参数的语法格式,而不需要使用原始的数组形式。在实际的调用中,int类型的参数3、4和5组成的数组被传入到了normalMethod的参数arg3中。
代码清单2-37 asVarargsCollector方法的使用示例

public class Varargs {
    public void normalMethod(String arg1,int arg2,int[]arg3){
        System.out.println(arg3);  // args 
    }
    @Test
    public void asVarargsCollector()throws Throwable{
        MethodHandles.Lookup lookup=MethodHandles.lookup();
        MethodHandle mh=lookup.findVirtual(Varargs.class,"normalMethod", MethodType.methodType(void.class, String.class,
                int.class, int[].class));
        mh = mh.asVarargsCollector(int[].class);
        mh.invoke(this,"Hello",2,1,4,5,7,8);
    }
}

asVarargsCollector方法的使用

4.2 MethodHandle的asCollector方法

  第二个方法asCollector的作用与asVarargsCollector类似,不同的是该方法只会把指定数量的参数;收集到原始方法句柄所对应的底层方法的数组类型参数中,而不像asVarargsCollector那样可以收集任意数量的参数。
  如代码清单2-38所示,还是以引用normalMethod的方法句柄为例,asCollector方法调用时的指定参数为2,即只有2个参数会被收集到整数类型数组中。在实际的调用中,int类型的参数3和4组成的数组被传入到了normalMethod的参数args中。

代码清单2-38 asCollector方法的使用示例

public class Varargs {
    public void normalMethod(String arg1,int arg2,int[]arg3){
        System.out.println(arg3);
    }
    @Test
    public void asCollector()throws Throwable{
        MethodHandles.Lookup lookup=MethodHandles.lookup();
        MethodHandle mh=lookup.findVirtual(Varargs.class,"normalMethod", MethodType.methodType(void.class, String.class,
                int.class, int[].class));
        mh = mh.asCollector(int[].class,2);
        mh.invoke(this,"Hello",2,1,4);
//        mh.invoke(this,"Hello",2,1,4,5,7,8); // 报错了指定最后一个入参数组的长度为2
    }
}

asCollector方法的使用

4.3MethodHandle的asSpreader方法

  上面的两个方法把数组类型参数转换为长度可变的参数,自然还有与之对应的执行反方向转换的方法。
  代码清单2-39给出的asSpreader方法就把长度可变的参数转换成数组类型的参数。转换之后的新方法句柄在调用时使用数组作为参数,而数组中的元素会被按顺序分配给原始方法句柄中的各个参数。在实际的调用中,toBeSpreaded方法所接受到的参数arg2、arg3和arg4的值分别是3、4和5。

代码清单2-39 asSpreader方法的使用示例

    public void toBeSpreaded (String arg1,int arg2,int arg3,int arg4){

        
    }

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

4.3MethodHandle的asFixedArity方法

  最后一个方法asFixedArity是把参数长度可变的方法转换成参数长度不变的方法。经过这样的转换之后,最后一个长度可变的参数实际上就变成了对应的数组类型。在调用方法句柄的时候,就只能使用数组来进行参数传递。
  如代码清单2-40所示,asFixedArity会把引用参数长度可变方法varargsMethod的原始方法句柄转换成固定长度参数的方法句柄。

代码清单2-40 asFixedArity方法的使用示例

 public void varargsMethod(String arg1,int...args){

    }

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

5.参数绑定

  在前面介绍过,如果方法句柄在调用时引用的底层方法不是静态的,调用的第一个参数应该是该方法调用的接收者。这个参数的值一般在调用时指定,也可以事先进行绑定。通过MethodHandle的bindTo方法可以预先绑定底层方法的调用接收者,在实际调用的时候,只需要传入实际参数即可,不需要再指定方法的接收者。
  代码清单2-41给出了对引用String类的length方法的方法句柄的两种调用方式:

  • 第一种没有进行绑定,调用时需要传入length方法的接收者;
  • 第二种方法预先绑定了一个String类的对象,因此调用时不需要再指定。

代码清单2-41 参数绑定的基本用法

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

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

  实际上,MethodHandle的bindTo方法只是绑定方法句柄的第一个参数而已,并不要求这个参数一定表示方法调用的接收者。对于一个MethodHandle,可以多次使用bindTo方法来为其中的多个参数绑定值。代码清单2-42给出了多次绑定的一个示例。方法句柄所引用的底层方法是String类中的indexOf方法,同时为方法句柄的前两个参数分别绑定了具体的值。
代码清单2-42 多次参数绑定的示例

@Test
    public void multipleBindTo()throws Throwable{
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        MethodHandle mh = lookup.findVirtual(String.class,"indexOf",MethodType.methodType(
        int.class, String.class, int.class));
        mh = mh.bindTo("Hello").bindTo("l");
        int index = "Hello".indexOf('l',2); 
        assertEquals(index, mh.invoke(2)); // true

}

  需要注意的是,在进行参数绑定的时候,只能对引用类型的参数进行绑定。无法为int和float这样的基本类型绑定值。对于包含基本类型参数的方法句柄,可以先使用wrap方法把方法类型中的基本类型转换成对应的包装类,再通过方法句柄的asType将其转换成新的句柄。转换之后的新句柄就可以通过bindTo来进行绑定,如代码清单2-43所示。

代码清单2-43 基本类型参数的绑定方式

    @Test
    public void multipleBindTo()throws Throwable{
        MethodHandles.Lookup lookup = MethodHandles.lookup();
//        MethodHandle mh = lookup.findVirtual(String.class,"indexOf",MethodType.methodType(
//        int.class, String.class, int.class));
//        mh = mh.bindTo("Hello").bindTo("l");
//        int index = "Hello".indexOf('l',2);
//        assertEquals(index, mh.invoke(2));
        MethodHandle mh=lookup.findVirtual(String.class,"substring",MethodType.methodType(String.class, int.class,
        int.class));
        mh=mh.asType(mh.type().wrap());
        mh=mh.bindTo("Hello World").bindTo(3);
        String  str = "Hello World".substring(3,5);
        System.out.println(mh.invoke(5));//值为“lo”
        assertEquals(str, mh.invoke(5));
    }

参考:
   https://www.baeldung.com/java-method-handles

参考资料:《java程序员修炼之道》、《深入理解java7核心技术与最佳实践》
posted @ 2020-05-28 20:11  笨拙的小菜鸟  阅读(3213)  评论(0编辑  收藏  举报