方法句柄的概念
方法句柄是一种指向方法的强类型、可执行的引用,它的类型由方法的参数类型和返回值类型组成,而与方法所在类名和方法名无关。
它作为方法的抽象,使方法调用不再受所在类型的约束,能更好的支持动态类型语言特性。同时它也使得函数可以在 java 中成为一等公民,可以作为方法参数进行传递。
看个例子:
class Horse {
public void race() {
System.out.println("Horse.race()");
}
}
class Deer {
public void race() {
System.out.println("Deer.race()");
}
}
class Cobra {
public void race() {
System.out.println("How do you turn this on?");
}
}
复制代码
如何用统一的方式调用 race 方法呢?
我们可以用反射,也可以抽象出一个包含 race 方法的接口,但这两种方式都不直接,要么有性能方面的损耗,要么增加了接口约束。
这个场景使用方法句柄就很合适,将 race 方法抽象成方法句柄,它们的句柄类型一致。这样调用者统一对接 race 方法的句柄,间接调用 race 方法。这其实跟使用接口统一交互界面是同样的道理,只不过方法句柄通过 MethodType 对象定义了方法的类型,不需要我们额外自定义类型。
方法句柄方式的参考实现:
public class RaceMethodHandleDemo {
public static void startRace(Object obj) throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle mh = lookup.findVirtual(obj.getClass(), "race", MethodType.methodType(void.class));
mh.invoke(obj);
}
public static void main(String[] args) throws Throwable {
startRace(new Horse());
startRace(new Deer());
}
}
复制代码
方法句柄类型示例
我们先练习几个小案例,一方面加深对句柄类型的理解,同时也体验下句柄的增删改操作。
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
public class MethodHandleTypeDemo {
public static void a(String s) {
System.out.println("a: " + s);
}
public void b(String s) {
System.out.println("b: " + s);
}
public static int c(String s1, String s2) {
System.out.println("c: arg0: " + s1 + ", arg1:" + s2);
return 1;
}
public static void main(String[] args) throws Throwable {
MethodHandles.Lookup l = MethodHandles.lookup();
MethodHandle mh0 = l.findStatic(MethodHandleTest.class, "a", MethodType.methodType(void.class, String.class));
System.out.println("mh0's type: " + mh0.type());
mh0.invokeExact("mh0");
MethodHandle mh1 = l.findVirtual(MethodHandleTest.class, "b", MethodType.methodType(void.class, String.class));
System.out.println("mh1's type: " + mh1.type());
mh1.invokeExact(new MethodHandleTest(), "mh1");
MethodHandle mh2 = mh1.bindTo(new MethodHandleTest());
System.out.println("mh2's type: " + mh2.type());
mh2.invokeExact("mh2");
MethodHandle mh3 = l.findStatic(MethodHandleTest.class, "c", MethodType.methodType(int.class, String.class, String.class));
System.out.println("mh3's type: " + mh3.type());
int c = (int) mh3.invokeExact("mh3-1", "mh3-2");
System.out.println("mh3 invokeExact result: " + c);
MethodHandle mh4 = MethodHandles.insertArguments(mh3, 0, "mh4-1");
System.out.println("mh4's type: " + mh4.type());
int c2 = (int) mh4.invokeExact("mh4-2");
System.out.println("mh4 invokeExact result: " + c2);
MethodHandle mh5 = MethodHandles.dropArguments(mh3, 1, String.class);
System.out.println("mh5's type: " + mh5.type());
int c3 = (int) mh5.invokeExact("mh5-1", "mh5-2", "mh5-3");
System.out.println("mh5 invokeExact result: " + c3);
MethodHandle mh6 = mh0.asType(MethodType.methodType(void.class, Object.class));
System.out.println("mh6's type: " + mh6.type());
mh6.invokeExact((Object) "mh6");
}
}
复制代码
执行结果如下:
mh0's type: (String)void
a: mh0
mh1's type: (MethodHandleTypeDemo,String)void
b: mh1
mh2's type: (String)void
b: mh2
mh3's type: (String,String)int
c: arg0: mh3-1, arg1:mh3-2
mh3 invokeExact result: 1
mh4's type: (String)int
c: arg0: mh4-1, arg1:mh4-2
mh4 invokeExact result: 1
mh5's type: (String,String,String)int
c: arg0: mh5-1, arg1:mh5-3
mh5 invokeExact result: 1
mh6's type: (Object)void
a: mh6
复制代码
根据打印结果,我们可以得到以下几点认识:
实例方法的第一个参数是实例对象,也需包含在方法句柄类型中(对应 mh1);
静态方法使用 MethodHandles.Lookup 的 findStatic 方法,实例方法使用 MethodHandles.Lookup 的 findVirtual,跟普通的方法调用字节码指令相对应(invokestatic 和 invokevirtual);
通过 bindTo 绑定实例对象,产生的适配器句柄类型则不包含对象类型了(对比 mh1 和 mh2),这个方法在我们通过 invokedynamic 调用点链接到合法句柄类型时会使用到。
MethodHandles.insertArguments 方法可以在指定位置绑定一个参数,我们可以通过它实现方法的柯里化。比如可以将 f(x, y)方法的 x 参数绑定为 3,生成另一个方法句柄 g(y) = f(x, y),每当调用 g(y)时会先在第一个位置插入 3,再调用 f(x, y)的方法句柄。
MethodHandles.dropArguments 可以删除指定位置的参数,生成新的方法句柄,原理同 insertArguments。
MethodHandle.asType 则可以修改参数类型,为原方法句柄生成适配器。
方法句柄的权限
方法句柄也有权限问题,但与反射在方法调用时检查不同,它是在句柄创建阶段进行检查的,如果多次调用句柄,它比反射可以省下权限重复检查的开销。
但需注意的是,句柄的访问权限不取决于创建句柄的位置,而是 Lookup 对象的位置。
实例演示:
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
public class MethodHandleAuthorityTest {
public static void main(String[] args) throws Throwable {
MethodHandles.Lookup l1 = Person.getLookup();
MethodHandle mh1 = l1.findGetter(Person.class, "name", String.class);
System.out.println((String) mh1.invokeExact(new Person()));
MethodHandles.Lookup l2 = MethodHandles.lookup();
MethodHandle mh2 = l2.findGetter(Person.class, "name", String.class);
System.out.println((String) mh2.invokeExact(new Person()));
}
}
class Person {
private String name = "Mary";
public static MethodHandles.Lookup getLookup() {
return MethodHandles.lookup();
}
}
复制代码
结果如下:
Mary
Exception in thread "main" java.lang.IllegalAccessException: member is private: com.mh.Person.name/java.lang.String/getField, from com.mh.MethodHandleAuthorityTest
at java.lang.invoke.MemberName.makeAccessException(MemberName.java:850)
at java.lang.invoke.MethodHandles$Lookup.checkAccess(MethodHandles.java:1536)
at java.lang.invoke.MethodHandles$Lookup.checkField(MethodHandles.java:1484)
at java.lang.invoke.MethodHandles$Lookup.getDirectFieldCommon(MethodHandles.java:1698)
at java.lang.invoke.MethodHandles$Lookup.getDirectField(MethodHandles.java:1688)
at java.lang.invoke.MethodHandles$Lookup.findGetter(MethodHandles.java:1027)
at com.mh.MethodHandleAuthorityTest.main(MethodHandleAuthorityTest.java:19)
复制代码
方法句柄的调用
句柄调用有两种方式:invokeExact 和 invoke。
invokeExact 会严格检查句柄类型跟传入参数类型,如果不一致就会报错。比如句柄类型要求 Object,你传的参数为 String,句柄调用时会报类型不匹配异常。这个情况下,需要将 String 显示强转成 Object 类型。
这里涉及到一个签名多态性(signature polymorphism)的概念(暂且认为签名等同于方法描述符)。
public final native @PolymorphicSignature Object invokeExact(Object... args) throws Throwable;
复制代码
可以看到 invokeExact 方法有一个 @PolymorphicSignature 注解标识。java 虚拟机在处理被该注解标识的方法时,会根据实际参数类型生成方法描述符,而不是句柄目标方法声明的方法描述符。比如:
public void test(MethodHandle mh, String s) throws Throwable {
mh.invokeExact(s);
mh.invokeExact((Object) s);
}
// 该方法对应的字节码
public void test(java.lang.invoke.MethodHandle, java.lang.String) throws java.lang.Throwable;
descriptor: (Ljava/lang/invoke/MethodHandle;Ljava/lang/String;)V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: aload_1
1: aload_2
2: invokevirtual #2 // Method java/lang/invoke/MethodHandle.invokeExact:(Ljava/lang/String;)V
5: aload_1
6: aload_2
7: invokevirtual #3 // Method java/lang/invoke/MethodHandle.invokeExact:(Ljava/lang/Object;)V
10: return
复制代码
当句柄调用 invokeExact 时会检查 invokevirtual 对应的方法描述符跟句柄的目标方法类型,如果不一致就会报类型不一致异常。
如果要自动适配参数类型,就需要调用 invoke 方法。invoke 方法会通过调用 MethodHandle.asType 方法生成一个句柄的适配器,该适配器会适配参数后再调用原方法句柄,同时对返回结果也会适配后再返回给用户。当然成功执行的前提是参数和返回结果都能够适配成功,即两个类型能够做类型转换。
方法句柄的实现
前面提到 jvm 会根据实际参数生成方法描述符,那这个描述符对应的方法是哪个呢?我们来实例验证一下。
在不加-XX:+ShowHiddenFrames 虚拟机参数之前,代码及执行结果如下:
import java.lang.invoke.*;
// -XX:+UnlockDiagnosticVMOptions -XX:+ShowHiddenFrames -Djava.lang.invoke.MethodHandle.DUMP_CLASS_FILES=true
public class Foo {
public static void bar(Object o) {
new Exception().printStackTrace();
}
public static void main(String[] args) throws Throwable {
MethodHandles.Lookup l = MethodHandles.lookup();
MethodType t = MethodType.methodType(void.class, Object.class);
MethodHandle mh = l.findStatic(Foo.class, "bar", t);
mh.invokeExact(new Object());
}
}
// 输出结果
java.lang.Exception
at Foo.bar(Foo.java:13)
at Foo.main(Foo.java:20)
复制代码
竟然是直接调用了目标方法?
且慢,在下结论之前,我们需要确保我们拿到的是真实的信息。
显然直接调用目标方法是不行的,因为需要做类型检查,必然需要在调用目标方法之前有所动作。所以显然是虚拟机隐藏了一些信息。
我们可以加上-XX:+ShowHiddenFrames 参数将隐藏的栈帧打印出来,这回结果如下:
java.lang.Exception
at Foo.bar(Foo.java:13)
at java.base/java.lang.invoke.DirectMethodHandle$Holder.invokeStatic(DirectMethodHandle$Holder:1000010)
at java.base/java.lang.invoke.LambdaForm$MH/0x0000000800b42840.invokeExact_MT(LambdaForm$MH:1000019)
at Foo.main(Foo.java:20)
复制代码
可以看到 invokeExact 方法调用了一个 LambdaForm 的方法,这是一个共享的、跟方法句柄类型相关的特殊适配器。我们可以通过-Djava.lang.invoke.MethodHandle.DUMP_CLASS_FILES=true 参数将它导出来:
final class LambdaForm$MH000 {
@Hidden
@Compiled
@ForceInline
static void invokeExact_MT000_LLL_V(Object var0, Object var1, Object var2) {
MethodHandle var3;
Invokers.checkExactType(var3 = (MethodHandle)var0, (MethodType)var2);
Invokers.checkCustomized(var3);
var3.invokeBasic(var1);
}
static void dummy() {
String var10000 = "MH.invokeExact_MT000_LLL_V=Lambda(a0:L,a1:L,a2:L)=>{\n t3:V=Invokers.checkExactType(a0:L,a2:L);\n t4:V=Invokers.checkCustomized(a0:L);\n t5:V=MethodHandle.invokeBasic(a0:L,a1:L);void}";
}
}
// 字节码
static void invokeExact_MT000_LLL_V(java.lang.Object, java.lang.Object, java.lang.Object);
descriptor: (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)V
flags: (0x0008) ACC_STATIC
Code:
stack=2, locals=3, args_size=3
0: aload_0
1: checkcast #14 // class java/lang/invoke/MethodHandle
4: dup
5: astore_0
6: aload_2
7: checkcast #16 // class java/lang/invoke/MethodType
10: invokestatic #22 // Method java/lang/invoke/Invokers.checkExactType:(Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)V
13: aload_0
14: invokestatic #26 // Method java/lang/invoke/Invokers.checkCustomized:(Ljava/lang/invoke/MethodHandle;)V
17: aload_0
18: aload_1
19: invokevirtual #30 // Method java/lang/invoke/MethodHandle.invokeBasic:(Ljava/lang/Object;)V
22: return
复制代码
可以看到,这个适配器会先调用 Invokers.checkExactType 方法检查参数类型,然后调用 Invokers.checkCustomized 方法。后者会在方法句柄调用多次后进行优化(对应参数-Djava.lang.invoke.MethodHandle.CUSTOMIZE_THRESHOLD,默认值为 127),最后调用方法句柄的 invokeBasic 方法。
invokeBasic 方法会调用至方法句柄本身持有的适配器中(也是一个 LambdaForm),我们可以通过反射将它打印出来:
Field field = MethodHandle.class.getDeclaredField("form");
field.setAccessible(true);
Object obj = field.get(mh);
System.out.println(obj.toString());
复制代码
DMH.invokeStatic_L_V=Lambda(a0:L,a1:L)=>{
t2:L=DirectMethodHandle.internalMemberName(a0:L);
t3:V=MethodHandle.linkToStatic(a1:L,t2:L);void}
复制代码
该适配器会访问方法句柄中的 MemberName 对象,将该对象作为参数调用方法句柄的 linkToStatic 方法。该方法会根据 MemberName 对象中的方法地址或方法表索引跳转至目标方法。
final class MemberName implements Member, Cloneable {
...
//@Injected JVM_Method* vmtarget;
//@Injected int vmindex;
...
复制代码
最后再补充说下 Invokers.checkCustomized 方法的作用。因为一开始为方法句柄生成的适配器是共享的,当方法句柄调用多次后,java 虚拟机会为它生成一个特有的适配器,该适配器将方法句柄当成常量,直接拿它的 MemberName 对象,然后调用 linkToStatic 方法。
我们可以将-Djava.lang.invoke.MethodHandle.CUSTOMIZE_THRESHOLD 设置为 0,此时生成的适配器就是特有的:
final class LambdaForm$DMH000 {
@Hidden
@Compiled
@ForceInline
static void invokeStatic000_LL_V(Object var0, Object var1) {
MethodHandle var3 = (MethodHandle)"CONSTANT_PLACEHOLDER_1 <<Foo.bar(Object)void/invokeStatic>>";
Object var2 = DirectMethodHandle.internalMemberName(var3);
MethodHandle.linkToStatic(var1, (MemberName)var2);
}
static void dummy() {
String var10000 = "DMH.invokeStatic000_LL_V=Lambda(a0:L,a1:L)=>{\n t2:L=DirectMethodHandle.internalMemberName(a0:L);\n t3:V=MethodHandle.linkToStatic(a1:L,t2:L);void}";
}
}
// 对应字节码
static void invokeStatic000_LL_V(java.lang.Object, java.lang.Object);
descriptor: (Ljava/lang/Object;Ljava/lang/Object;)V
flags: (0x0008) ACC_STATIC
Code:
stack=2, locals=3, args_size=2
0: ldc #14 // String CONSTANT_PLACEHOLDER_1 <<Foo.bar(Object)void/invokeStatic>>
2: checkcast #16 // class java/lang/invoke/MethodHandle
5: astore_0
6: aload_0
7: invokestatic #22 // Method java/lang/invoke/DirectMethodHandle.internalMemberName:(Ljava/lang/Object;)Ljava/lang/Object;
10: astore_2
11: aload_1
12: aload_2
13: checkcast #24 // class java/lang/invoke/MemberName
16: invokestatic #28 // Method java/lang/invoke/MethodHandle.linkToStatic:(Ljava/lang/Object;Ljava/lang/invoke/MemberName;)V
19: return
复制代码
后续
方法句柄跟 invokedynamic 指令是什么关系?请听下回分解。
也欢迎关注我的公众号(搜索:Make IT Simple),一起学习交流。
▲ 欢迎关注“Make IT Simple”,一起搞定底层原理
评论