MethodHandle(方法句柄)
MethodHandle(方法句柄)系列之一:MethodHandle和MethodType
阅读此文章的作者建议先了解java反射和动态代理。
java7中为间接调用方法引入了新的api,其中最关键的是java.lang.invoke包,即方法句柄。我们可以看成是java反射的升级版,但是它不反射那样有时候显得冗长、繁重的粗糙。
方法句柄中首先涉及到两个重要的类,MethodHandle和MethodType。
MethodHandle
它是可对直接执行的方法(或域、构造方法等)的类型的引用,或者说,它是一个有能力安全调用方法的对象。换个方法来说,通过句柄我们可以直接调用该句柄所引用的底层方法。从作用上来看,方法句柄类似于反射中的Method类,但是方法句柄的功能更加强大、使用更加灵活、性能也更好。
MethodType
它是表示方法签名类型的不可变对象。每个方法句柄都有一个MethodType实例,用来指明方法的返回类型和参数类型。它的类型完全由参数类型和方法类型来确定,而与它所引用的底层的方法的名称和所在的类没有关系。举个例子,例如String类的length方法和Integer类的intValue方法的方法句柄的类型就是一样的,因为这两个方法都没有参数,而且返回值类型都是int,则我们可以通过下列语句获取同一个方法类型:MethodType mt = MethodType.methodType(int.class);
MethodType的对象实例只能通过MethodType类中的静态工厂方法来创建,而且MethodType类的所有对象实例都是不可变的,类似于String类。如果修改了MethodType实例中的信息,就会生成另外一个MethodType实例。
参考资料:《java程序员修炼之道》、《深入理解java7核心技术与最佳实践》
MethodHandle(方法句柄)系列之二:方法句柄的简单使用
/** * * @author LiuYeFeng<897908343@qq.com> * @date 2015年4月8日 下午10:41:13 * @CopyRight 2015 TopView Inc * @version V1.0 */ public class MethodHandleTest { public MethodHandle getHandler() { MethodHandle mh = null; MethodType mt = MethodType.methodType(String.class, int.class, int.class); MethodHandles.Lookup lk = MethodHandles.lookup(); try { mh = lk.findVirtual(String.class, "substring", mt); } catch (Throwable e) { e.printStackTrace(); } return mh; } public static void main(String[] args) throws Throwable { MethodHandle mh = new MethodHandleTest().getHandler(); String str = "hello world"; Object result1 = mh.invoke(str, 1, 3); Object result2 = (String) mh.invokeExact(str, 1, 3); // Object result2 = mh.invokeExact(str, new Integer(1), 3); /** * 上面这句方法执行时报错,因为方法类型为String.class, int.class, int.class * 而返回的类型为Object,与声明中为String不符合 * 其中第二个参数类型为Integer,与声明中为int不符合,则类型适配不符合,系统报错。 */ System.out.println("result 1:" + result1); System.out.println("result 1:" + result2); } }
代码输出结果均为el。
接下来说一下方法句柄的调用过程,首先,在获取方法句柄之前,先通过MethodType的静态工厂方法,先生成一个包含方法参数类型、方法返回类型的的方法类型,也就是MethodType mt = MethodType.methodType(String.
class
,
int
.
class
,
int
.
class
)。
其次,获取方法句柄要用到Lookup对象,比如代码中的MethodHandles.Lookup lk,这个对象可以提供其所在环境中任何可见方法的方法句柄。我们可以将其比喻成包含有某个类对象的方法成员、方法的容器,通过
lk.findVirtual(String.
class
,
"substring"
, mt);
具体锁定String类型中的某个方法,作为方法句柄返回。要从lookup对象中得到方法句柄,需要给出持有所需方法的类,方法的名称,以及跟方法相匹配的方法类型。
最后,获取到方法句柄后,我们就可以通过方法句柄来调用底层的方法,这点上,跟反射中的方法调用类似。方法句柄提供两个方法调用底层方法,invoke和invokeExact方法。invokeExact方法与直接调用底层方法是一样的,比如代码中,mh.invoke(str, 1, 2)就相当于直接调用str.substring(1, 3);具体两个方法的区别下面再说。这里先解析一下这两个方法的调用,方法的调用参数基本都一样,第一个参数为方法的接受对象,即是哪个对象执行这个方法,接下来的参数就是执行方法所需要的参数。当然第一个参数,也就是方法的接受对象,可以通过方法句柄的bindto动态的参数绑定方法来绑定,从而使方法句柄的调用和普通方法调用没区别,动态的参数绑定我们以后再研究。
这里需要强调一下,静态方法和动态方法之间的差别,静态方法是不需要制定方法的接受对象的,而一般方法是需要的。
/** * * @author LiuYeFeng<897908343@qq.com> * @date 2015年4月8日 下午10:41:13 * @CopyRight 2015 TopView Inc * @version V1.0 */ public class MethodHandleTest { public MethodHandle getHandler() { MethodHandle mh = null; MethodType mt = MethodType.methodType(String.class, int.class, int.class); MethodHandles.Lookup lk = MethodHandles.lookup(); try { mh = lk.findVirtual(String.class, "substring", mt); } catch (Throwable e) { e.printStackTrace(); } return mh; } public static void main(String[] args) throws Throwable { MethodHandle mh = new MethodHandleTest().getHandler(); String str = "hello world"; Object result1 = mh.invoke(str, 1, 3); Object result2 = (String) mh.invokeExact(str, 1, 3); // Object result2 = mh.invokeExact(str, new Integer(1), 3); /** * 上面这句方法执行时报错,因为方法类型为String.class, int.class, int.class * 而返回的类型为Object,与声明中为String不符合 * 其中第二个参数类型为Integer,与声明中为int不符合,则类型适配不符合,系统报错。 */ System.out.println("result 1:" + result1); System.out.println("result 1:" + result2); } }
invoke和invokeExact方法的区别,从名字上来看,明显是后者准确性更高,或者说要求更严格。invokeExact方法在调用时要求严格的类型匹配,方法的返回值类型也在考虑范围之内,如同上面代码中注释掉的一句。如果把
Object result2 = (String) mh.invokeExact(str,
1
,
3
);
中的(String)去掉类型转换的话,在调用的时候该方法会认为返回值是Object类型而不是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;
第1、2、3条很好理解,第4、5、6条感觉原理都一样,我就用个新例子说明。
public class MethodHandleTest { public MethodHandle getHandler() { MethodHandle mh = null; MethodType mt = MethodType.methodType(void.class); MethodHandles.Lookup lk = MethodHandles.lookup(); try { mh = lk.findVirtual(MethodHandleTest.class, "print", mt); } catch (Throwable e) { e.printStackTrace(); } return mh; } public void print() { System.out.println("print"); } public static void main(String[] args) throws Throwable { MethodHandleTest mht = new MethodHandleTest(); MethodHandle mh = mht.getHandler(); int result1 = (int) mh.invoke(mht); Object result2 = mh.invoke(mht); System.out.println("result 1:" + result1); System.out.println("result 2:" + result2); } }
程序输出结果是:
result 1:0
result 2:null