现在的位置: 首页 > 综合 > 正文

JDK7的动态类型,关于java.lang.invoke包的解释

2018年12月15日 ⁄ 综合 ⁄ 共 30985字 ⁄ 字号 评论关闭

来源:深入理解Java 7:核心技术与最佳实践

方法句柄(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类对象是否相等,只取决于它们所包含的参数类型和返回值类型是否完全一致。

MethodType类的对象实例只能通过MethodType类中的静态工厂方法来创建。这样的工厂方法有三类。第一类是通过指定参数和返回值的类型来创建MethodType,这主要是使用methodType方法的多种重载形式。使用这些方法的时候,至少需要指定返回值类型,而参数类型则可以是0到多个。返回值类型总是出现在methodType方法参数列表的第一个,后面紧接着的是0到多个参数的类型。类型都是由Class类的对象来指定的。如果返回值类型是void,可以用void.class或java.lang.Void.class来声明。代码清单2-31中给出了使用methodType方法的几个示例。每个MethodType声明上以注释的方式给出了与之相匹配的String类中的一个方法。这里值得一提的是,最后一个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);
}

除了显式地指定返回值和参数的类型之外,还可以生成通用的MethodType类型,即返回值和所有参数的类型都是Object类。这是通过静态工厂方法genericMethodType来创建的。方法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);
}

最后介绍的一个工厂方法是比较复杂的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类。如果不指定,默认使用系统类加载器。

在通过工厂方法创建出MethodType类的对象实例之后,可以对其进行进一步修改。这些修改都围绕返回值和参数类型展开。所有这些修改方法都返回另外一个新的MethodType对象。代码清单2-34给出了对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);
}

除了上面这几个精确修改返回值和参数的类型的方法之外,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对象,可以很容易地通过方法级联来简化代码。

2.方法句柄的调用

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

最直接的调用一个方法句柄的做法是通过invokeExact方法实现的。这个方法与直接调用底层方法是完全一样的。invokeExact方法的参数依次是作为方法接收者的对象和调用时候的实际参数列表。比如在代码清单2-36中,先获取String类中substring的方法句柄,再通过invokeExact来进行调用。这种调用方式就相当于直接调用"Hello World".substring(1, 3)。关于方法句柄的获取,下一节会具体介绍。

代码清单2-36 使用invokeExact方法调用方法句柄
public void invokeExact() throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType type = MethodType.methodType(String.class, int.class, int.class);
MethodHandle mh = lookup.findVirtual(String.class, "substring", type);
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会认为方法的返回值类型是void,也不同于方法句柄要求的String类型的返回值。

与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。

满足上面规则时进行两个方法类型之间的转换是会成功的。对于invoke方法的具体使用,只需要把代码清单2-36中的invokeExact方法换成invoke即可,并不需要做太多的介绍。

最后一种调用方式是使用invokeWithArguments。该方法在调用时可以指定任意多个Object类型的参数。完整的调用方式是首先根据传入的实际参数的个数,通过MethodType的genericMethodType方法得到一个返回值和参数类型都是Object的新方法类型。再把原始的方法句柄通过asType转换后得到一个新的方法句柄。最后通过新方法句柄的invokeExact方法来完成调用。这个方法相对于invokeExact和invoke的优势在于,它可以通过Java反射API被正常获取和调用,而invokeExact和invoke不可以这样。它可以作为反射API和方法句柄之间的桥梁。

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

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

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

代码清单2-37 asVarargsCollector方法的使用示例
public void normalMethod(String arg1, int arg2, int[] arg3) {
}

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, 3, 4, 5);
}

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

代码清单2-38 asCollector方法的使用示例
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, 3, 4);
}

上面的两个方法把数组类型参数转换为长度可变的参数,自然还有与之对应的执行反方向转换的方法。代码清单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});
}

最后一个方法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});
}

4.参数绑定

在前面介绍过,如果方法句柄在调用时引用的底层方法不是静态的,调用的第一个参数应该是该方法调用的接收者。这个参数的值一般在调用时指定,也可以事先进行绑定。通过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 多次参数绑定的示例
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");
System.out.println(mh.invoke(2)); //值为2
}

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

代码清单2-43 基本类型参数的绑定方式
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);
System.out.println(mh.invoke(5)); //值为“lo”

5.获取方法句柄

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

方法句柄的查找是通过java.lang.invoke.MethodHandles.Lookup类来完成的。在查找之前,需要通过调用MethodHandles.lookup方法获取到一个MethodHandles.Lookup类的对象。MethodHandles.Lookup类提供了一些方法以根据不同的条件进行查找。代码清单2-44以String类为例说明了查找构造方法和一般方法的示例。方法findConstructor用来查找类中的构造方法,需要指定返回值和参数类型,即MethodType对象。而findVirtual和findStatic则用来查找一般方法和静态方法,除了表示方法的返回值和参数类型的MethodType对象之外,还需要指定方法的名称。

代码清单2-44 查找构造方法、一般方法和静态方法的方法句柄的示例
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));
}

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

代码清单2-45 查找类中特殊方法的方法句柄的示例
public MethodHandle lookupSpecial() throws NoSuchMethodException, IllegalAccessException, Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle mh = lookup.findSpecial(MethodHandleLookup.class, "privateMethod", MethodType.methodType(void.class), MethodHandleLookup.class);
return mh;
}

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

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

代码清单2-46  查找类中的静态域和一般域对应的获取和设置的方法句柄的示例
public void lookupFieldAccessor() throws NoSuchFieldException, Illegal-AccessException{
MethodHandles.Lookup lookup = MethodHandles.lookup();
lookup.findGetter(Sample.class, "name", String.class);
lookup.findSetter(Sample.class, "name", String.class);
lookup.findStaticGetter(Sample.class, "value", int.class);
lookup.findStaticSetter(Sample.class, "value", int.class);
}

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

除了直接在某个类中进行查找之外,还可以从通过反射API得到的Constructor、Field和Method等对象中获得方法句柄。如代码清单2-47所示,首先通过反射API得到表示构造方法的Constructor对象,再通过unreflectConstructor方法就可以得到其对应的一个方法句柄;而通过unreflect方法可以将Method类对象转换成方法句柄。对于私有方法,则需要使用unreflectSpecial来进行转换,同样也需要提供一个作用与findSpecial中参数相同的额外参数;对于Field类的对象来说,通过unreflectGetter和unreflectSetter就可以得到获取和设置其值的方法句柄。

代码清单2-47 通过反射API获取方法句柄的示例
public void unreflect() throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
Constructor constructor = String.class.getConstructor(byte[].class);
lookup.unreflectConstructor(constructor);
Method method = String.class.getMethod("substring", int.class, int.class);
lookup.unreflect(method);

Method privateMethod = ReflectMethodHandle.class.getDeclaredMethod("privateMethod");
lookup.unreflectSpecial(privateMethod, ReflectMethodHandle.class);

Field field = ReflectMethodHandle.class.getField("name");
lookup.unreflectGetter(field);
lookup.unreflectSetter(field);
}

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

第一个方法是用来对数组进行操作的,即得到可以用来获取和设置数组中元素的值的方法句柄。这些工厂方法的作用等价于2.2.4节介绍的反射API中的java.lang.reflect.Array类中的静态方法。如代码清单2-48所示,MethodHandles的arrayElementGetter和arrayElementSetter方法分别用来得到获取和设置数组元素的值的方法句柄。调用这些方法句柄就可以对数组进行操作。

代码清单2-48 获取和设置数组中元素的值的方法句柄的使用示例
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); //值为6
}

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

代码清单2-49 MethodHandles类的identity方法的使用示例
public void identity() throws Throwable {
MethodHandle mh = MethodHandles.identity(String.class);
String value = (String) mh.invoke("Hello"); //值为"Hello"
}

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

代码清单2-50 MethodHandles类的constant方法的使用示例
public void constant() throws Throwable {
MethodHandle mh = MethodHandles.constant(String.class, "Hello");
String value = (String) mh.invoke(); //值为"Hello"
}

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

6.方法句柄变换

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

首先介绍对参数进行处理的变换方法。在调用变换之后的新方法句柄时,调用时的参数值会经过一定的变换操作之后,再传递给原始的方法句柄来完成具体的执行。

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

如代码清单2-51所示,原始的方法句柄mhOld引用的是String类中的substring方法,其类型是String类的返回值加上两个int类型的参数。在调用dropArguments方法的时候,第一个参数表示待变换的方法句柄,第二个参数指定的是要添加的新参数类型在原始参数列表中的起始位置,其后的多个参数类型将被添加到参数列表中。新的方法句柄mhNew的参数类型变为float、String、String、int和int,而在实际调用时,前面两个参数的值会被忽略掉。可以把这些多余的参数理解成特殊调用模式所需要的占位符。

代码清单2-51 dropArguments方法的使用示例
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);
}

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

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

代码清单2-52 insertArguments方法的使用示例
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--”
}

第三个方法filterArguments的作用是可以对方法句柄调用时的参数进行预处理,再把预处理的结果作为实际调用时的参数。预处理的过程是通过其他的方法句柄来完成的。可以对一个或多个参数指定用来进行处理的方法句柄。代码清单2-53给出了filterArguments方法的使用示例。要执行的原始方法句柄所引用的是Math类中的max方法,而在实际调用时传入的却是两个字符串类型的参数。中间的参数预处理是通过方法句柄mhGetLength来完成的,该方法句柄的作用是获得字符串的长度。这样就可以把字符串类型的参数转换成原始方法句柄所需要的整数类型。完成预处理之后,将处理的结果交给原始方法句柄来完成调用。

代码清单2-53 filterArguments方法的使用示例
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
}

在使用filterArguments的时候,第二个参数和后面的可变长度的方法句柄参数是配合起来使用的。第二个参数指定的是进行预处理的方法句柄需要处理的参数在参数列表中的起始位置。紧跟在后面的是一系列对应的完成参数预处理的方法句柄。方法句柄与它要处理的参数是一一对应的。如果希望跳过某些参数不进行处理,可以使用null作为方法句柄的值。在进行预处理的时候,要注意预处理方法句柄和原始方法句柄之间的类型匹配。如果预处理方法句柄用于对某个参数进行处理,那么该方法句柄只能有一个参数,而且参数的类型必须匹配所要处理的参数的类型;其返回值类型需要匹配原始方法句柄中对应的参数类型。只有类型匹配,才能用方法句柄对实际传入的参数进行预处理,再把预处理的结果作为原始方法句柄调用时的参数来使用。

第四个方法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方法的使用示例
public static int targetMethod(int arg1, int arg2, int arg3) {
return arg1;
}

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(Transform.class, "targetMethod", typeTarget);
MethodHandle mhResult = MethodHandles.foldArguments(mhTarget, mhCombiner);
int value = (int) mhResult.invoke(3, 4); //输出为4
}

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

第五个方法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表示第二个)。

第六个方法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 = MethodHandles.lookup();
MethodType typeTarget = MethodType.methodType(int.class, String.class);
MethodHandle mhParseInt = lookup.findStatic(Integer.class, "parseInt", typeTarget);
MethodType typeHandler = MethodType.methodType(int.class, Exception.class, String.class);
MethodHandle mhHandler = lookup.findVirtual(Transform.class, "handleException", typeHandler).bindTo(this);
MethodHandle mh = MethodHandles.catchException(mhParseInt, NumberFormatException.class, mhHandler);
mh.invoke("Hello");
}

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

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

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

代码清单2-57 guardWithTest方法的使用示例
public static boolean guardTest() {
return Math.random() > 0.5;
}

public void guardWithTest() throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle mhTest = lookup.findStatic(Transform.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
}

除了可以在变换的时候对方法句柄的参数进行处理之外,还可以对方法句柄被调用后的返回值进行修改。对返回值进行处理是通过filterReturnValue方法来实现的。原始的方法句柄被调用之后的结果会被传递给另外一个方法句柄进行再次处理,处理之后的结果被返回给调用者。代码清单2-58展示了filterReturnValue的用法。原始的方法句柄mhSubstring所引用的是String类的substring方法,对返回值进行处理的方法句柄mhUpperCase所引用的是String类的toUpperCase方法。通过filterReturnValue方法得到的新方法句柄的运行效果是将调用substring得到的子字符串转换成大写的形式。

代码清单2-58 filterReturnValue方法的使用示例
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
}

7.特殊方法句柄

在有些情况下,可能会需要对一组类型相同的方法句柄进行同样的变换操作。这个时候与其对所有的方法句柄都进行重复变换,不如创建出一个可以用来调用其他方法句柄的方法句柄。这种特殊的方法句柄的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方法得到的方法句柄被称为“元方法句柄”,具有调用其他方法句柄的能力。

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

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

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

代码清单2-61 使用方法句柄实现接口的示例
public void doSomething() {
System.out.println("WORK");
}

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类中的方法。实际的方法个数可能不止一个,可能包含同一方法的不同重载形式。

9.访问控制权限

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

之前介绍过,通过MethodHandles.Lookup类的方法可以查找类中已有的方法以得到MethodHandle对象。而MethodHandles.Lookup类的对象本身则是通过MethodHandles类的静态方法lookup得到的。在Lookup对象被创建的时候,会记录下当前所在的类(称为查找类)。只要查找类能够访问某个方法或域,就可以通过Lookup的方法来查找到对应的方法句柄。代码清单2-62给出了一个访问控制权限相关的示例。AccessControl类中的accessControl方法返回了引用其中私有方法privateMethod的方法句柄。由于当前查找类可以访问该私有方法,因此查找过程是成功的。其他类通过调用accessControl得到的方法句柄就可以调用这个私有方法。虽然其他类不能直接访问AccessControl类中的私有方法,但是在调用方法句柄的时候不会进行访问控制权限检查,因此对方法句柄的调用可以成功进行。

代码清单2-62 方法句柄查找时的访问控制权限
public class AccessControl {
private void privateMethod() {
System.out.println("PRIVATE");
}

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

10. 交换点

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

在代码清单2-63中,在调用guardWithTest方法的时候指定在交换点有效的时候调用方法句柄mhMin,而在无效的时候则调用mhMax。guardWithTest方法的返回值是一个新的方法句柄mhNew。交换点在初始时处于有效状态,因此mhNew在第一次调用时使用的是mhMin,结果为3。在通过invalidateAll方法把交换点设成无效状态之后,再次调用mhNew时实际调用的方法句柄就变成了mhMax,结果为4。

代码清单2-63 交换点的使用示例
public void useSwitchPoint() throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType type = MethodType.methodType(int.class, int.class, int.class);
MethodHandle mhMax = lookup.findStatic(Math.class, "max", type);
MethodHandle mhMin = lookup.findStatic(Math.class, "min", type);
SwitchPoint sp = new SwitchPoint();
MethodHandle mhNew = sp.guardWithTest(mhMin, mhMax);
mhNew.invoke(3, 4); //值为3
SwitchPoint.invalidateAll(new SwitchPoint[] {sp});
mhNew.invoke(3, 4); //值为4
}

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

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

通过上面章节对方法句柄的详细介绍可以看出,方法句柄是一个非常灵活的对方法进行操作的轻量级结构。方法句柄的作用类似于在某些语言中出现的函数指针(function pointer)。在程序中,方法句柄可以在对象之间自由传递,不受访问控制权限的限制。方法句柄的这种特性,使得在Java语言中也可以进行函数式编程。下面通过几个具体的示例来进行说明。

第一个示例是对数组进行操作。数组作为一个常见的数据结构,有的编程语言提供了对它进行复杂操作的功能。这些功能中比较常见的是forEach、map和reduce操作等。这些操作的语义并不复杂,forEach是对数组中的每个元素都依次执行某个操作,而map则是把原始数组按照一定的转换过程变成一个新的数组,reduce是把一个数组按照某种规则变成单个元素。这些操作在其他语言中可能比较好实现,而在Java语言中,则需要引入一些接口,由此带来的是繁琐的实现和冗余的代码。有了方法句柄之后,这个实现就变得简单多了。代码清单2-64给出了使用方法句柄的forEach、map和reduce方法的实现。对数组中元素的处理是由一个方法句柄来完成的。对这个方法句柄只有类型的要求,并不限制它所引用的底层方法所在的类或名称。

代码清单2-64 使用方法句柄实现数组操作的示例
private static final MethodType typeCallback = MethodType.methodType(Object.class, Object.class, int.class);

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

public static Object[] map(Object[] array, MethodHandle handle) throws Throwable {
Object[] result = new Object[array.length];
for (int i = 0, len = array.length; i < len; i++) {
result[i] = handle.invoke(array[i], i);
}
return result;
}

public static Object reduce(Object[] array, Object initalValue, MethodHandle handle) throws Throwable {
Object result = initalValue;
for (int i = 0, len = array.length; i < len; i++) {
result = handle.invoke(result, array[i]);
}
return result;
}

第二个例子是方法的柯里化(currying)。柯里化的含义是对一个方法的参数值进行预先设置之后,得到一个新的方法。比如一个做加法运算的方法,本来有两个参数,通过柯里化把其中一个参数的值设为5之后,得到的新方法就只有一个参数。新方法的运行结果是用5加上这个唯一的参数的值。通过MethodHandles类中的insertArguments方法可以很容易地实现方法句柄的柯里化。代码清单2-65给出了相关的实现。方法curry负责把一个方法句柄的第一个参数的值设为指定值;add方法就是一般的加法操作;add5方法对引用add的方法句柄进行柯里化,得到新的方法句柄,再调用此方法句柄。

代码清单2-65 使用方法句柄实现的柯里化
public static MethodHandle curry(MethodHandle handle, int value) {
return MethodHandles.insertArguments(handle, 0, value);
}

public static int add(int a, int b) {
return a + b;
}

public static int add5(int a) throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType type = MethodType.methodType(int.class, int.class, int.class);
MethodHandle mhAdd = lookup.findStatic(Curry.class, "add", type);
MethodHandle mh = curry(mhAdd, 5);
return (int) mh.invoke(a);
}

上面给出的这两个示例所实现的功能虽然比较简单,但是反映出了方法句柄在使用时的极大灵活性。配合方法句柄支持的变换操作,可以实现很多有趣和实用的功能。


【上篇】
【下篇】

抱歉!评论已关闭.