java方法句柄-----2.方法句柄的获取、变换、特殊方法句柄

1.获取方法句柄

  获取方法句柄最直接的做法是从一个类中已有的方法中转换而来,得到的方法句柄直接引用这个底层方法。在上一篇文章的示例中都是通过这种方式来获取方法句柄的。方法句柄可以按照与反射API类似的做法,从已有的类中根据一定的条件进行查找。
  与反射API不同的是,方法句柄并不区分构造方法、方法和域,而是统一转换成MethodHandle对象。对于域来说,获取到的是用来获取和设置该域的值的方法句柄。
  方法句柄的查找是通过java.lang.invoke.MethodHandles.Lookup类来完成的。在查找之前,需要通过调用MethodHandles.lookup方法获取到一个MethodHandles.Lookup类的对象。MethodHandles.Lookup类提供了一些方法以根据不同的条件进行查找。

1.1查找构造方法、一般方法和静态方法的方法句柄

代码清单2-44以String类为例说明了查找构造方法和一般方法的示例。

  • 方法findConstructor用来查找类中的构造方法,需要指定返回值和参数类型,即MethodType对象。
  • findVirtual和findStatic则用来查找一般方法和静态方法,除了表示方法的返回值和参数类型的MethodType对象之外,还需要指定方法的名称。
    代码清单2-44 查找构造方法、一般方法和静态方法的方法句柄的示例
    @Test
    public void lookupMethod()throws NoSuchMethodException, IllegalAccessException{
        MethodHandles.Lookup lookup=MethodHandles.lookup();
//构造方法
        lookup.findConstructor(String.class, MethodType.methodType(void.class, byte[].class));
//String.substring
        lookup.findVirtual(String.class,"substring",MethodType.methodType(String.class, int.class, int.class));
//String.format
        lookup.findStatic(String.class,"format",MethodType.methodType(String.class,String.class, Object[].class));
    }

1.2 查找类中的特殊方法(类中的私有方法)

  除了上面3种类型的方法之外,还有一个findSpecial方法用来查找类中的特殊方法,主要是类中的私有方法。代码清单2-45给出了findSpecial的使用示例,Method-HandleLookup是lookupSpecial方法所在的类,而privateMethod是该类中的一个私有方法。由于访问的是类的私有方法,从访问控制的角度出发,进行方法查找的类需要具备访问私有方法的权限。
代码清单2-45 查找类中特殊方法的方法句柄的示例

       private String privateInfo(){
        return "10";
    }
    @Test
    public void findSpecial() throws Throwable {
        MethodHandles.Lookup lookup=MethodHandles.lookup();
        MethodHandle mh = lookup.findSpecial(Varargs.class, "privateInfo", MethodType.methodType(String.class),Varargs.class);
        System.out.println(mh.invoke(new Varargs())); // 10

    }

  从上面的代码中可以看到,findSpecial方法比之前的findVirtual和findStatic等方法多了一个参数。这个额外的参数用来指定私有方法被调用时所使用的类。提供这个类的原因是为了满足对私有方法的访问控制的要求。当方法句柄被调用时,指定的调用类必须具备访问私有方法的权限,否则会出现无法访问的错误

1.3 查找类中静态域和一般域

  除了类中本来就存在的方法之外,对域的处理也是通过相应的获取和设置域的值的方法句柄来完成的。
代码清单2-46说明了如何查找到类中的静态域和一般域所对应的获取和设置的方法句柄。
在查找的时候只需要提供域所在的类的Class对象、域的名称和类型即可。

public class Sample {
//    private String name;   member is private: com.baeldung.java9.methodhandles.Sample.name/java.lang.String/getField,
    public String name ;
    public static  int value ;
    @Test
    public void lookupFieldAccessor() throws Throwable {
         Sample smp = new Sample();
        MethodHandles.Lookup lookup=MethodHandles.lookup();
        MethodHandle mh = lookup.findGetter(Sample.class,"name",String.class);
        MethodHandle mh1 = lookup.findSetter(Sample.class,"name",String.class);
        String res = (String) mh.invoke(smp);
        System.out.println(res);
        mh1 = mh1.bindTo(smp);
        mh1.invoke("万平");
        res = (String) mh.invoke(smp);
        System.out.println(res); // setter之后getter
        MethodHandle mh3 = lookup.findStaticGetter(Sample.class,"value",int.class);
        MethodHandle mh4 =lookup.findStaticSetter(Sample.class,"value",int.class);
        int value = (int) mh3.invoke();
        System.out.println(value);
        mh4.invoke(15);
        value = (int) mh3.invoke();
        System.out.println(value);  // 静态鱼setter之后getter
        lookup.findGetter(Sample.class,"age",int.class);
    }
}
       
控制台打印:    
null
万平
0
15
Disconnected from the target VM, address: '127.0.0.1:53655', transport: 'socket'
java.lang.NoSuchFieldException: no such field: com.baeldung.java9.methodhandles.Sample.age/int/getField

对于静态域来说,调用其对应的获取和设置值的方法句柄时,并不需要提供调用的接收者对象作为参数。而对于一般域来说,该对象在调用时是必需的。

1.4 通过反射API得到的Constructor、Field和Method等对象中获得方法句柄

  除了直接在某个类中进行查找之外,还可以从通过反射API得到的Constructor、Field和Method等对象中获得方法句柄。
如代码清单2-47所示,
  首先通过反射API得到表示构造方法的Constructor对象,接着

  • 通过unreflectConstructor方法就可以得到其对应的一个方法句柄;
  • 通过unreflect方法可以将Method类对象转换成方法句柄。
  • 对于私有方法,则需要使用unreflectSpecial来进行转换,同样也需要提供一个作用与findSpecial中参数相同的额外参数;
  • 对于Field类的对象来说,通过unreflectGetter和unreflectSetter就可以得到获取和设置其值的方法句柄。

代码清单2-47 通过反射API获取方法句柄的示例

public class Sample {
    public String name ;
    public static  int value ;
    public String testMethod(int arg1,int arg2){
        System.out.println(arg1+","+arg2);
        return arg1+","+arg2;
    }

    private String privateMethod(int arg1,int arg2){
        System.out.println(arg1+","+arg2);
        return arg1+","+arg2;
    }

    @Test
    public void unreflect()throws Throwable{
        MethodHandles.Lookup lookup=MethodHandles.lookup();
        Constructor constructor=String.class.getConstructor(byte[].class);
        MethodHandle mh1 = lookup.unreflectConstructor(constructor);
        Method method=String.class.getMethod("substring",int.class, int.class);
        MethodHandle mh2 = lookup.unreflect(method);
        Method privateMethod=Sample.class.getDeclaredMethod("privateMethod",int.class, int.class);
        MethodHandle mh3 =lookup.unreflectSpecial(privateMethod, Sample.class);
        MethodHandle mh4 = lookup.unreflect(method);
        Field field=Sample.class.getField("name");
        MethodHandle mh5 = lookup.unreflectGetter(field);
    }
}

1.5 通过java.lang.invoke.MethodHandles中提供的一些静态工厂方法来创建一些通用的方法句柄。

1.5.1 得到可以用来获取和设置数组中元素的值的方法句柄

  第一个方法是用来对数组进行操作的,即得到可以用来获取和设置数组中元素的值的方法句柄。
  如代码清单2-48所示,MethodHandles的arrayElementGetter和arrayElementSetter方法分别用来得到获取和设置数组元素的值的方法句柄。调用这些方法句柄就可以对数组进行操作。

代码清单2-48 获取和设置数组中元素的值的方法句柄的使用示例
public void

 @Test
    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); // set array[4] = 6;
        MethodHandle getter=MethodHandles.arrayElementGetter(int[].class);
        int value=(int)getter.invoke(array,3);//值为6    get array[3] ;
    }

1.5.2 静态方法identity

  MethodHandles中的静态方法identity的作用是通过它所生成的方法句柄,在每次调用的时候,总是返回其输入参数的值。如代码清单2-49所示,在使用identity方法的时候只需要传入方法句柄的唯一参数的类型即可,该方法句柄的返回值类型和参数类型是相同的。

代码清单2-49 MethodHandles类的identity方法的使用示例

    @Test
   public void identity()throws Throwable{
       MethodHandle mh=MethodHandles.identity(String.class);
       String value=(String)mh.invoke("Hello");//值为"Hello"
   }

1.5.3 静态方法constan

  方法constant的作用则更加简单,在生成的时候指定一个常量值,以后这个方法句柄被调用的时候,总是返回这个常量值,在调用时也不需要提供任何参数。这个方法提供了一种把一个常量值转换成方法句柄的方式,如下面的代码所示。在调用constant方法的时候,只需要提供常量的类型和值即可

代码清单2-50 MethodHandles类的constant方法的使用示例

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

  MethodHandles类中的identity方法和constant方法的作用类似于在开发中用到的“空对象(Null object)”模式的应用。在使用方法句柄的某些场合中,如果没有合适的方法句柄对象,可能不允许直接用null来替换,这个时候可以通过这两个方法来生成简单无害的方法句柄对象作为替代。

2 方法句柄变换

  方法句柄的强大之处在于可以对它进行各种不同的变换操作。这些变换操作包括对方法句柄的返回值参数的处理等,同时这些单个的变换操作可以组合起来,形成复杂的变换过程。所有的这些变换方法都是MethodHandles类中的静态方法。这些方法一般接受一个已有的方法句柄对象作为变换的来源,而方法的返回值则是变换操作之后得到的新的方法句柄。下面的内容中经常出现的“原始方法句柄”表示的是变换之前的方法句柄,而“新方法句柄”则表示变换之后的方法句柄。

2.1 对参数进行处理的变换方法

2.1.1MethodHandles.dropArguments

  在调用变换之后的新方法句柄时,调用时的参数值会经过一定的变换操作之后,再传递给原始的方法句柄来完成具体的执行。
  第一个方法dropArguments可以在一个方法句柄的参数中添加一些无用的参数。这些参数虽然在实际调用时不会被使用,但是它们可以使变换之后的方法句柄的参数类型格式符合某些所需的特定模式。这也是这种变换方式的主要应用场景。
代码清单2-51所示,原始的方法句柄mhOld引用的是String类中的substring方法,其类型是String类的返回值加上两个int类型的参数。在调用dropArguments方法的时候,第一个参数表示待变换的方法句柄,第二个参数指定的是要添加的新参数类型在原始参数列表中的起始位置,其后的多个参数类型将被添加到参数列表中。
  新的方法句柄mhNew的参数类型变为float、String、String、int和int,而在实际调用时,前面两个参数的值会被忽略掉。可以把这些多余的参数理解成特殊调用模式所需要的占位符。

代码清单2-51 dropArguments方法的使用示例

    @Test
    public void dropArguments()throws Throwable{
        MethodHandles.Lookup lookup=MethodHandles.lookup();
        MethodType type=MethodType.methodType(String.class, int.class, int.class);
        MethodHandle mhOld=lookup.findVirtual(String.class,"substring",type);
        String value=(String)mhOld.invoke("Hello",2,3);
        MethodHandle mhNew=MethodHandles.dropArguments(mhOld,0,float.class, String.class);
        value=(String)mhNew.invoke(0.5f,"Ignore","Hello",2,3);
    }

2.1.2 MethodHandles.insertArguments

  第二个方法insertArguments的作用与本小节前面提到的MethodHandle的bindTo方法类似,但是此方法的功能更加强大。这个方法可以同时为方法句柄中的多个参数预先绑定具体的值。在得到的新方法句柄中,已经绑定了具体值的参数不再需要提供,也不会出现在参数列表中。

代码清单2-52中,方法句柄mhOld所表示的底层方法是String类中的concat方法。
  在调用insertArguments方法的时候,与上面的dropArguments方法类似,从第二个参数所指定的参数列表中的位置开始,用其后的可变长度的参数的值作为预设值,分别绑定到对应的参数上。在这里把mhOld的第二个参数的值预设成了固定值“--”,其作用是在调用新方法句柄时,只需要传入一个参数即可,相当于总是与“--”进行字符串连接操作,即使用“--”作为后缀。由于有一个参数被预先设置了值,因此mhNew在调用时只需要一个参数即可。如果预先绑定的是方法句柄mhOld的第一个参数,那就相当于用字符串“--”来连接各种不同的字符串,即为字符串添加“--”作为前缀。如果insertArguments方法调用时指定了多个绑定值,会按照第二个参数指定的起始位置,依次进行绑定。

代码清单2-52 insertArguments方法的使用示例

 @Test
    public void insertArguments()throws Throwable{
        MethodHandles.Lookup lookup=MethodHandles.lookup();
        MethodType type=MethodType.methodType(String.class, String.class);
        MethodHandle mhOld=lookup.findVirtual(String.class,"concat",type);
        String value=(String)mhOld.invoke("Hello","World");
        MethodHandle mhNew=MethodHandles.insertArguments(mhOld,1,"--");
        value=(String)mhNew.invoke("Hello");//值为“Hello--”
    }

2.1.3 MethodHandles.filterArguments

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

代码清单2-53给出了filterArguments方法的使用示例。

  要执行的原始方法句柄所引用的是Math类中的max方法,而在实际调用时传入的却是两个字符串类型的参数。中间的参数预处理是通过方法句柄mhGetLength来完成的,该方法句柄的作用是获得字符串的长度。这样就可以把字符串类型的参数转换成原始方法句柄所需要的整数类型。完成预处理之后,将处理的结果交给原始方法句柄来完成调用。

 @Test
    public void filterArguments()throws Throwable{
        MethodHandles.Lookup lookup=MethodHandles.lookup();
        MethodType type=MethodType.methodType(int.class, int.class, int.class);
        MethodHandle mhGetLength=lookup.findVirtual(String.class,"length",MethodType.methodType(int.class));
        MethodHandle mhTarget=lookup.findStatic(Math.class,"max",type);
        MethodHandle mhNew=MethodHandles.filterArguments(mhTarget,0,mhGetLength, mhGetLength);
        int value=(int)mhNew.invoke("Hello","New World");//值为9
    }

2.1.4 MethodHandles.foldArguments

  第四个方法foldArguments的作用与filterArguments很类似,都是用来对参数进行预处理的.不同之处在于,foldArguments对参数进行预处理之后的结果,不是替换掉原始的参数值,而是添加到原始参数列表的前面,作为一个新的参数。当然,如果参数预处理的返回值是void,则不会添加新的参数。另外,参数预处理是由一个方法句柄完成的,而不是像filterArguments那样可以由多个方法句柄来完成。这个方法句柄会负责处理根据它的类型确定的所有可用参数。
代码清单2-54中原始的方法句柄引用的是静态方法targetMethod,而用来对参数进行预处理的方法句柄mhCombiner引用的是Math类中的max方法。变换之后的新方法句柄mhResult在被调用时,两个参数3和4首先被传递给句柄mhCombiner所引用的Math.max方法,返回值是4。这个返回值被添加到原始调用参数列表的前面,即得到新的参数列表4、3、4。这个新的参数列表会在调用时被传递给原始方法句柄mhTarget所引用的targetMethod方法。

代码清单2-54 foldArguments方法的使用示例

    @Test
    public static int targetMethod(int arg1,int arg2,int arg3){
        return arg1;
    }

    @Test
    public void foldArguments()throws Throwable{
        MethodHandles.Lookup lookup=MethodHandles.lookup();
        MethodType typeCombiner=MethodType.methodType(int.class, int.class, int.class);
        MethodHandle mhCombiner=lookup.findStatic(Math.class,"max",typeCombiner);
        MethodType typeTarget=MethodType.methodType(int.class, int.class, int.class, int.class);
        MethodHandle mhTarget=lookup.findStatic(Varargs.class,"targetMethod",typeTarget);
        MethodHandle mhResult=MethodHandles.foldArguments(mhTarget, mhCombiner);
        int value=(int)mhResult.invoke(3,4);//输出为4
    }

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

2.1.5 MethodHandles.permuteArguments

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

代码清单2-55给出了一个对参数进行完全排列的示例。代码中的原始方法句柄mhCompare所引用的是Integer类中的compare方法。当使用参数3和4进行调用的时候,返回值是-1。通过permuteArguments方法把参数的排列顺序进行颠倒,得到了新的方法句柄mhNew。再用同样的参数调用方法句柄mhNew时,返回结果就变成了1,因为传递给底层compare方法的实际调用参数变成了4和3。新方法句柄mhDuplicateArgs在通过permuteArguments方法进行变换的时候,重复了第二个参数,因此传递给底层compare方法的实际调用参数是4和4,返回的结果是0。

代码清单2-55 permuteArguments方法的使用示例

public void permuteArguments()throws Throwable{
MethodHandles.Lookup lookup=MethodHandles.lookup();
MethodType type=MethodType.methodType(int.class, int.class, int.class);
MethodHandle mhCompare=lookup.findStatic(Integer.class,"compare",type);
int value=(int)mhCompare.invoke(3,4);//值为-1
MethodHandle mhNew=MethodHandles.permuteArguments(mhCompare, type,1,0);
value=(int)mhNew.invoke(3,4);//值为1
MethodHandle mhDuplicateArgs=MethodHandles.permuteArguments(mhCompare, type,1,1);
value=(int)mhDuplicateArgs.invoke(3,4);//值为0
}

在这里还要着重介绍一下permuteArguments方法的参数。第二个参数表示的是重新排列完成之后的新方法句柄的类型。紧接着的是多个用来表示新的排列顺序的整数。这些整数的个数必须与原始句柄的参数个数相同。整数出现的位置及其值就表示了在排列顺序上的对应关系。比如在上面的代码中,创建方法句柄mhNew的第一个整数参数是1,这就表示调用原始方法句柄时的第一个参数的值实际上是调用新方法句柄时的第二个参数(编号从0开始,1表示第二个)。

2.1.6 MethodHandles.catchExceptions

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

如代码清单2-56所示,原始的方法句柄mhParseInt所引用的是Integer类中的parseInt方法,这个方法在字符串无法被解析成数字时会抛出java.lang.Number-FormatException。用来进行异常处理的方法句柄是mhHandler,它引用了当前类中的handleException方法。通过catchException得到的新方法句柄mh在被调用时,如果抛出了NumberFormatException,则会调用handleException方法。

代码清单2-56 catchException方法的使用示例

public int handleException(Exception e, String str){
System.out.println(e.getMessage());
return 0;
} public void catchExceptions()throws Throwable{
MethodHandles.Lookup lookup=Method Handles.lookup();
MethodType typeTarget=MethodType.methodType(int.class, String.class);
MethodHandle mhParseInt=lookup.find Static(Integer.class,"parseInt",typeTarget);
MethodType typeHandler=Method Type.method Type(int.class, Exception.class, String.class);
Method Handlemh Handler=lookup.find Virtual(Transform.class,"handle Exception",type Handler).bindTo(this);
Method Handlemh=Method Handles.catch Exception(mhParseInt, Number Format Exception.class, mhHandler);
mh.invoke("Hello");
}

在这里需要注意几个细节:原始方法句柄和异常处理方法句柄的返回值类型必须是相同的,这是因为当产生异常的时候,异常处理方法句柄的返回值会作为调用的结果;而在两个方法句柄的参数方面,异常处理方法句柄的第一个参数是它所处理的异常类型,其他参数与原始方法
句柄的参数相同。在异常处理方法句柄被调用的时候,其对应的底层方法可以得到原始方法句柄调用时的实际参数值。在上面的例子中,当handleException方法被调用的时候,参数e的值是NumberFormatException类的对象,参数str的值是原始的调用值“Hello”;在获得异常处理方法句柄的时候,使用了bindTo方法。这是因为通过findVirtual找到的方法句柄的第一个参数类型表示的是方法调用的接收者,这与catchException要求的第一个参数必须是异常类型的约束不相符,因此通过bindTo方法来为第一个参数预先绑定值。这样就可以得到所需的正确的方法句柄。当然,如果异常处理方法句柄所引用的是静态方法,就不存在这个问题。

2.1.7 MethodHandles.guardWithTest

  最后一个在对方法句柄进行变换时与参数相关的方法是guardWithTest。这个方法可以实现在方法句柄这个层次上的条件判断的语义,相当于if-else语句。使用guardWithTest时需要提供3个不同的方法句柄:第一个方法句柄用来进行条件判断,而剩下的两个方法句柄则分别在条件成立和不成立的时候被调用。用来进行条件判断的方法句柄的返回值类型必须是布尔型,而另外两个方法句柄的类型则必须一致,同时也是生成的新方法句柄的类型。

如代码清单2-57所示,进行条件判断的方法句柄mhTest引用的是静态guardTest方法,在条件成立和不成立的时候调用的方法句柄则分别引用了Math类中的max方法和min方法。由于guardTest方法的返回值是随机为true或false的,所以两个方法句柄的调用也是随机选择的。

代码清单2-57 guardWithTest方法的使用示例

    @Test
    public static boolean guardTest(){
        return Math.random() > 0.5;
    }

    @Test
    public void guardWithTest()throws Throwable{
        MethodHandles.Lookup lookup=MethodHandles.lookup();
        MethodHandle mhTest=lookup.findStatic(Varargs.class,"guardTest",MethodType.methodType(boolean.class));
        MethodType type=MethodType.methodType(int.class, int.class, int.class);
        MethodHandle mhTarget=lookup.findStatic(Math.class,"max",type);
        MethodHandle mhFallback=lookup.findStatic(Math.class,"min",type);
        MethodHandle mh=MethodHandles.guardWithTest(mhTest, mhTarget, mhFallback);
        int value=(int)mh.invoke(3,5);//值随机为3或5
    }

2.2方法句柄被调用后的返回值进行修改

2.2.1 MethodHandles.filterReturnValue

  除了可以在变换的时候对方法句柄的参数进行处理之外,还可以对方法句柄被调用后的返回值进行修改。对返回值进行处理是通过filterReturnValue方法来实现的。原始的方法句柄被调用之后的结果会被传递给另外一个方法句柄进行再次处理,处理之后的结果被返回给调用者。

代码清单2-58展示了filterReturnValue的用法。原始的方法句柄mhSubstring所引用的是String类的substring方法,对返回值进行处理的方法句柄mhUpperCase所引用的是String类的toUpperCase方法。通过filterReturnValue方法得到的新方法句柄的运行效果是将调用
substring得到的子字符串转换成大写的形式。

代码清单2-58 filterReturnValue方法的使用示例

    @Test
    public void filterReturnValue()throws Throwable{
        MethodHandles.Lookup lookup=MethodHandles.lookup();
        MethodHandle mhSubstring=lookup.findVirtual(String.class,"substring",MethodType.methodType(String.class, int.class));
        MethodHandle mhUpperCase=lookup.findVirtual(String.class,"toUpperCase",MethodType.methodType(String.class));
        MethodHandle mh=MethodHandles.filterReturnValue(mhSubstring, mhUpperCase);
        String str=(String)mh.invoke("Hello World",5);//输出WORLD
    }

3.特殊方法句柄

3.1 MethodHandles.invoker 方法的使用

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

  代码清单2-59给出了invoker方法的使用示例。首先invoker方法句柄可以调用的方法句柄类型的返回值类型为String,加上3个类型分别为Object、int和int的参数。两个被调用的方法句柄,其中一个引用的是String类中的substring方法,另外一个引用的是当前类中的testMethod方法。这两个方法都可以通过invoke方法来正确调用。

代码清单2-59 invoker方法的使用示例

public void invoker()throws Throwable{
MethodType typeInvoker=MethodType.methodType(String.class, Object.class, int.class, int.class);
MethodHandle invoker=MethodHandles.invoker(typeInvoker);
MethodType typeFind=MethodType.methodType(String.class, int.class, int.class);
MethodHandles.Lookup lookup=MethodHandles.lookup();
MethodHandle mh1=lookup.findVirtual(String.class,"substring",typeFind);
MethodHandle mh2=lookup.findVirtual(InvokerUsage.class,"testMethod",typeFind);
String result=(String)invoker.invoke(mh1,"Hello",2,3);
result=(String)invoker.invoke(mh2,this,2,3);
}

而exactInvoker的使用与invoker非常类似,这里就不举例说明了。

  上面提到了使用invoker和exactInvoker的一个重要好处就是在对这个方法句柄进行变换之后,所得到的新方法句柄在调用其他方法句柄的时候,这些变换操作都会被自动地引用,而不需要对每个所调用的方法句柄再单独应用。如代码清单2-60所示,通过filterReturnValue为通过exactInvoker得到的方法句柄添加变换操作,当调用方法句柄mh1的时候,这个变换会被自动应用,使作为调用结果的字符串自动变成大写形式。

代码清单2-60 invoker和exactInvoker对方法句柄变换的影响

public void invokerTransform()throws Throwable{
MethodType typeInvoker=MethodType.methodType(String.class, String.class, int.class, int.class);
MethodHandle invoker=MethodHandles.exactInvoker(typeInvoker);
MethodHandles.Lookup lookup=MethodHandles.lookup();
MethodHandle mhUpperCase=lookup.findVirtual(String.class,"toUpperCase",MethodType.methodType(String.class));
invoker=MethodHandles.filterReturnValue(invoker, mhUpperCase);
MethodType typeFind=MethodType.methodType(String.class, int.class, int.class);
MethodHandle mh1=lookup.findVirtual(String.class,"substring",typeFind);
String result=(String)invoker.invoke(mh1,"Hello",1,4);//值为“ELL”
}

通过invoker方法和exactInvoker方法得到的方法句柄被称为“元方法句柄”,具有调用其他方法句柄的能力。

4.使用方法句柄实现接口

  2.3节介绍的动态代理机制可以在运行时为多个接口动态创建实现类,并拦截通过接口进行的方法调用。方法句柄也具备动态实现一个接口的能力。这是通过java.lang.invoke.MethodHandleProxies类中的静态方法asInterfaceInstance来实现的。不过通过方法句柄来实现接口所受的限制比较多。首先该接口必须是公开的,其次该接口只能包含一个名称唯一的方法。这样限制是因为只有一个方法句柄用来处理方法调用。
  调用asInterfaceInstance方法时需要两个参数,第一个参数是要实现的接口类,第二个参数是处理方法调用逻辑的方法句柄对象。方法的返回值是一个实现了该接口的对象。当调用接口的方法时,这个调用会被代理给方法句柄来完成。方法句柄的返回值作为接口调用的返回值。接口的方法类型与方法句柄的类型必须是兼容的,否则会出现异常。

代码清单2-61是使用方法句柄实现接口的示例。被代理的接口是java.lang.Runnable,其中仅包含一个run方法。实现接口的方法句柄引用的是当前类中的doSomething方法。在调用asInterfaceInstance之后得到的Runnable接口的实现对象被用来创建一个新的线程。该线程运行之后发现doSomething方法会被调用。这是由于当Runnable接口的run方法被调用的时候,方法句柄mh也会被调用。

代码清单2-61 使用方法句柄实现接口的示例

public class UseMethodHandleProxies {
    public void doSomething(){
        System.out.println("WORK");
    }

    @Test
    public void useMethodHandleProxy()throws Throwable{
        MethodHandles.Lookup lookup= MethodHandles.lookup();
        MethodHandle mh=lookup.findVirtual(UseMethodHandleProxies.class,"doSomething",MethodType.methodType(void.class));
        mh=mh.bindTo(this);
        Runnable runnable= MethodHandleProxies.asInterfaceInstance(Runnable.class, mh);
        new Thread(runnable).start();
    }
}

  通过方法句柄来实现接口的优势在于不需要新建额外的Java类,只需要复用已有的方法即可。在上面的示例中,任何已有的不带参数和返回值的方法都可以用来实现Runnable接口。需要注意的是,要求接口所包含的方法的名称唯一,不考虑Object类中的方法。实际的方法个数可能不止一个,可能包含同一方法的不同重载形式。

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

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