当我们提起java lambda表达式的实现时,第一反应一定是:这就是java的语法糖啊,它实现肯定还是匿名内部类啊!
但是,事实真的是这样么?不要着急,我们做个试验先。众所周知,想要在Android中实现View的点击回调有好几种写法,其中有两种分别是匿名内部类和lambda表达式,首先看匿名内部类:
接下来是lambda表达式:
看到有什么不同了么? 使用匿名内部类的方式,当使用外部类引用时使用的名称为外部类名.this
,这是java基础知识,就不再赘述了。但是!!! 使用lambda表达式,当时用外部类引用时名称为this
。这也就说明了lambda表达式的实现并不是匿名内部类。
那么问题来了,既然不是使用匿名内部类实现,那它的实现方式又是什么呢?本文将借着这个问题展开,解开lambda的神秘面纱。
1. lambda表达式在字节码中的表现
首先,按照老规矩,我们先写一个简单的java类如下:
public class Test {
private void startThread() {
Runnable r = () -> showToast();
Thread t = new Thread(r);
t.start();
}
private void showToast() {
System.out.println("I am lambda of virtual");
}
public static void main(String[] args) {
Runnable r = () -> System.out.println("I am lambda of static");
Thread t = new Thread(r);
t.start();
Test test = new Test();
test.startThread();
}
}
编译运行后会生成.class文件,使用javap工具查看其字节码,
javap -verbose -p Test.class
其中 -p的意思是展示所有的类和成员。
使用javap反编译后,生成的字节码很长,我们只看关键部分,首先看一下main方法:
在我们使用lambda调用的地方,生成了一条invokedynamic指令,根据上篇关于常量池表的介绍可知,其对应的常量池表的符号引用为#9,是一个CONSTANT_InvokeDynamic_info表类型,回顾一下CONSTANT_InvokeDynamic_info表结构:
项目 | 类型 | 描述 |
---|---|---|
tag | u1 | 值为18 |
bootstrap_method_attr_index | u2 | 值必须是对当前Class文件中引导方法表的bootstrap_methods[]数组的有效索引 |
name_and_type_index | u2 | 值必须是对当前常量池的有效索引,常量池在该索引处的项必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述符 |
再看一下常量池表#9对应的数据:
从CONSTANT_InvokeDynamic_info表结构我们可以知道,#1是BootstrapMethods的索引,对应#1的BootstrapMethod信息:
关于BootstrapMethod的解释:
每一个invokedynamic指令的实例叫做一个动态调用点(dynamic call site), 动态调用点最开始是 未链接状态(unlinked:表示还未指定该调用点要调用的方法), 动态调用点依靠引导方法(BootstrapMethod)来链接到具体的方法. 引导方法是由编译器生成, 在运行期当JVM第一次遇到invokedynamic指令时, 会调用引导方法来将invokedynamic指令所指定的名字(方法名,方法签名)和具体的执行代码(目标方法)链接起来, 引导方法的返回值永久的决定了调用点的行为.引导方法的返回值类型是java.lang.invoke.CallSite, 一个invokedynamic指令关联一个CallSite, 将所有的调用委托到CallSite当前的target(MethodHandle)
从上文的字节码也能验证这一解释,也就是说,当运行阶段执行至invokedynamic指令时,才会执行BootstrapMethod, 那么lambda表达式的执行肯定也和这一过程息息相关,接下来我们分析一下BootstrapMethod的执行
2. BootstrapMethod的执行
从上文#1对应的BootstrapMethod信息中可知,这个BootstrapMethod调用时,执行的是LambdaMetafactory.metafactory
方法, 这个方法的调用在CallSite#makeSite方法,由JVM调用。
invokeDynamic有一个特点是第一次运行时确定调用点后,便与该调用点绑定,绑定操作也是在这个方法中
部分代码:
其中,bootstrapMethod.invoke就是引导方法的调用,这里会链接到LambdaMetafactory.metafactory中执行,
先看一下LambdaMetafactory.metafactory方法的注释:
通过分派一个给定的MethodHandle,以实现接口的方式实现的简单快捷的“方法对象”构造器,在传入合适的类型和可变参数后确定类型。一般作为invokedynamic执行的动态调用点的引导方法,用来支持java语言中lambda表达式的实现。
这是标准的流式metaFactory,altMetafactory方法提供了额外的灵活性支持。
当调用次方法返回目标CallSite时,生成的function objects是一个类的实例,该类实现了invokedType类型的接口,用invokedName和通过samMethodType给定的签名生成一个方法,有可能也会重写基类的其他方法。参数:
caller: 表示一个具有调用者访问权限的上下文,当用于invokedynamic时,由虚拟机自动传入
invokedName: 需要实现的方法,当用于invokedynamic时,它由InvokeDynamic结构的NameAndType属性提供,并由虚拟机自动传入
invokedType: CallSite的预期签名,参数类型表示捕获变量(用于invokeVirtual所属对象的实例)的类型,返回类型是要实现的接口,当用于invokedynamic时,它由InvokeDynamic结构的NameAndType属性提供,并由虚拟机自动传入,如果实现方法是实例方法并且此签名具有任何参数,则调用签名中的第一个参数必须对应于接收方。
samMethodType: 要实现的方法的方法签名和返回类型
implMethod: 要实现的方法中需要调用的方法的MethodHandle
instantiatedMethodType: 要实现的方法的方法签名和返回类型, 可能和samMethodType相同,
返回值: 生成的动态调用点
看完注释什么感觉呢,感觉更懵逼了,这都是啥啊!还是先看看代码吧
还是Test类的案例,我们看一下在执行方法时,入参都是什么,在方法内部打个断点:
通过参数invokeName、invokeType、samMethodType可以确定lambda实现的方法为接口Runnable
的void run()
方法,而通过implMethod可以获取run方法中要调用的方法信息,本例中implMethod信息如下:
其中member是被调方法的方法签名,这里的名称为lambda$main$1
,这时肯定会有人发问了,我们写的代码里哪有这个方法了?怎么调用呢?其实,这个方法是编译器自动生成的,每一个lambda调用点都会生成一个类似的这种方法,方法体内部就是我们lambda语句块内的逻辑实现。而在本例中,两个lambda对应的方法长这样:
注意看flags属性,ACC_SYNTHETIC的意思就是由编译器生成,而生成的方法是不是static,则由lambda语句块内的具体逻辑决定。
new InnerClassLambdaMetafactory
语句的作用是初始化类中所需数据,从入参中提取方法的各种信息,我们就不分析源码了,有兴趣的同学可以自己写一个简单的demo debug看一下,还是挺有意思的。重点看buildCallSite
的逻辑
buildCallSite
的代码:
CallSite buildCallSite() throws LambdaConversionException {
// 通过ASM工具生成内部类
final Class> innerClass = spinInnerClass();
if (invokedType.parameterCount() == 0) {
final Constructor>[] ctrs = AccessController.doPrivileged(
new PrivilegedAction[]>() {
@Override
public Constructor>[] run() {
Constructor>[] ctrs = innerClass.getDeclaredConstructors();
if (ctrs.length == 1) {
ctrs[0].setAccessible(true);
}
return ctrs;
}
});
if (ctrs.length != 1) {
throw new LambdaConversionException("Expected one lambda constructor for "
+ innerClass.getCanonicalName() + ", got " + ctrs.length);
}
try {
// 生成CallSite实例
Object inst = ctrs[0].newInstance();
return new ConstantCallSite(MethodHandles.constant(samBase, inst));
}
catch (ReflectiveOperationException e) {
throw new LambdaConversionException("Exception instantiating lambda object", e);
}
} else {
try {
UNSAFE.ensureClassInitialized(innerClass);
return new ConstantCallSite(
MethodHandles.Lookup.IMPL_LOOKUP
.findStatic(innerClass, NAME_FACTORY, invokedType));
}
catch (ReflectiveOperationException e) {
throw new LambdaConversionException("Exception finding constructor", e);
}
}
}
代码的第一行spinInnerClass是生成一个内部类,实现metaFactory
方法入参invokedType
所标志的接口,看一下实现:
private Class> spinInnerClass() throws LambdaConversionException {
String[] interfaces;
String samIntf = samBase.getName().replace('.', '/');
boolean accidentallySerializable = !isSerializable && Serializable.class.isAssignableFrom(samBase);
if (markerInterfaces.length == 0) {
interfaces = new String[]{samIntf};
} else {
// Assure no duplicate interfaces (ClassFormatError)
Set itfs = new LinkedHashSet<>(markerInterfaces.length + 1);
itfs.add(samIntf);
for (Class> markerInterface : markerInterfaces) {
itfs.add(markerInterface.getName().replace('.', '/'));
accidentallySerializable |= !isSerializable && Serializable.class.isAssignableFrom(markerInterface);
}
interfaces = itfs.toArray(new String[itfs.size()]);
}
// 使用ASM工具的ClassWriter对象生成一个class
// CLASSFILE_VERSION: class版本号
// ACC_SUPER + ACC_FINAL + ACC_SYNTHETIC: 访问控制标记
// lambdaClassName: 类名,规则:targetClass.getName().replace('.', '/') + "$$Lambda$" + counter.incrementAndGet()
// interfaces: 要实现的接口,从从mataFactory方法入参invokedType的returnType中获取
cw.visit(CLASSFILE_VERSION, ACC_SUPER + ACC_FINAL + ACC_SYNTHETIC,
lambdaClassName, null,
JAVA_LANG_OBJECT, interfaces);
// 生成类的字段
for (int i = 0; i < argDescs.length; i++) {
FieldVisitor fv = cw.visitField(ACC_PRIVATE + ACC_FINAL,
argNames[i],
argDescs[i],
null, null);
fv.visitEnd();
}
// 生成构造方法
generateConstructor();
if (invokedType.parameterCount() != 0) {
// 生成类的工厂方法
generateFactory();
}
// 生成接口的实现方法
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, samMethodName,
samMethodType.toMethodDescriptorString(), null, null);
// 给实现方法添加注解
mv.visitAnnotation("Ljava/lang/invoke/LambdaForm$Hidden;", true);
// 写入lambda语句块的实现, 本例中在这里写入lambda$main$1的调用
new ForwardingMethodGenerator(mv).generate(samMethodType);
// Forward the bridges
if (additionalBridges != null) {
for (MethodType mt : additionalBridges) {
mv = cw.visitMethod(ACC_PUBLIC|ACC_BRIDGE, samMethodName,
mt.toMethodDescriptorString(), null, null);
mv.visitAnnotation("Ljava/lang/invoke/LambdaForm$Hidden;", true);
new ForwardingMethodGenerator(mv).generate(mt);
}
}
if (isSerializable)
generateSerializationFriendlyMethods();
else if (accidentallySerializable)
generateSerializationHostileMethods();
cw.visitEnd();
// Define the generated class in this VM.
final byte[] classBytes = cw.toByteArray();
// If requested, dump out to a file for debugging purposes
if (dumper != null) {
AccessController.doPrivileged(new PrivilegedAction() {
@Override
public Void run() {
dumper.dumpClass(lambdaClassName, classBytes);
return null;
}
}, null,
new FilePermission("<>", "read, write"),
// createDirectories may need it
new PropertyPermission("user.dir", "read"));
}
return UNSAFE.defineAnonymousClass(targetClass, classBytes, null);
}
这里面涉及到另一个比较宏大的话题:ASM, 它是一个字节码生成工具,后面我计划用专门的文章去介绍它,在这里先按下不表。
在生成lambda对应的内部类后,buildCallSite方法又实例化了该类的对象和一个CallSite对象,构造方法的入参为MethodHandle类型。
MethodHandle是JDK7时新加入的java.lang.invoke包的一个重要组成部分,这个包的主要目的是在之前单纯依靠符号引用来确定调用的目标方法这条路之外,提供一种新的动态确定目标方法的机制,称为“方法句柄”(MethodHandle),这种方式比较类似于C/C++的函数指针。有一些博客说invokedynamic确定动态调用是使用反射的方式,这是不正确的。
在生成CallSite对象后,JVM会继续执行刚才通过ASM生成的Class的对象实例的方法,达到动态调用的目标。
3. 查看生成的Class代码
上面的分析只是局限于理论上的推理,实际情况是不是这样呢? 我们需要做个验证
打开IDEA, 菜单的Run -> Edit Configurations -> 选择Add VM Options 加入如下配置:
-Djdk.internal.lambda.dumpProxyClasses=D:\zyl\demo\javademo\JavaTest\src\com\dafasoft\test
=号后面的值根据实际情况而定,这个配置的作用是保留jvm在运行期间生成的Class
运行程序后,多出如下两个class:
与我们上面的分析是一致的。
4. 总结:
lambda表达式的实现:
1.java文件在编译时遇到lambda表达式,会将其内部的逻辑块取出,封装于一个命名为lambda{编号}的方法中,方法是否为static由代码块和所在方法决定
- JVM 在第一次遇到invokedynamic指令时,会执行Bootstrap方法,通过Bootstrap方法的入参确定要实现的接口及其方法。
- Bootstrap方法通过接口及方法使用ASM动态生成对应的Class并注册到JVM中
- Bootstrap方法返回动态调用点CallSite, JVM通过CallSite调用对应的方法,完成一次lambda的调用。
5 附录
Test.java编译后生成的完整的字节码:
Classfile /D:/zyl/demo/javademo/JavaTest/out/production/JavaTest/com/dafasoft/test/Test.class
Last modified 2021-5-11; size 1745 bytes
MD5 checksum 18b6eb43641fa1295b58d1b1bcacc539
Compiled from "Test.java"
public class com.dafasoft.test.Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #15.#38 // java/lang/Object."":()V
#2 = InvokeDynamic #0:#43 // #0:run:(Lcom/dafasoft/test/Test;)Ljava/lang/Runnable;
#3 = Class #44 // java/lang/Thread
#4 = Methodref #3.#45 // java/lang/Thread."":(Ljava/lang/Runnable;)V
#5 = Methodref #3.#46 // java/lang/Thread.start:()V
#6 = Fieldref #47.#48 // java/lang/System.out:Ljava/io/PrintStream;
#7 = String #49 // I am lambda of virtual
#8 = Methodref #50.#51 // java/io/PrintStream.println:(Ljava/lang/String;)V
#9 = InvokeDynamic #1:#53 // #1:run:()Ljava/lang/Runnable;
#10 = Class #54 // com/dafasoft/test/Test
#11 = Methodref #10.#38 // com/dafasoft/test/Test."":()V
#12 = Methodref #10.#55 // com/dafasoft/test/Test.startThread:()V
#13 = String #56 // I am lambda of static
#14 = Methodref #10.#57 // com/dafasoft/test/Test.showToast:()V
#15 = Class #58 // java/lang/Object
#16 = Utf8
#17 = Utf8 ()V
#18 = Utf8 Code
#19 = Utf8 LineNumberTable
#20 = Utf8 LocalVariableTable
#21 = Utf8 this
#22 = Utf8 Lcom/dafasoft/test/Test;
#23 = Utf8 startThread
#24 = Utf8 r
#25 = Utf8 Ljava/lang/Runnable;
#26 = Utf8 t
#27 = Utf8 Ljava/lang/Thread;
#28 = Utf8 showToast
#29 = Utf8 main
#30 = Utf8 ([Ljava/lang/String;)V
#31 = Utf8 args
#32 = Utf8 [Ljava/lang/String;
#33 = Utf8 test
#34 = Utf8 lambda$main$1
#35 = Utf8 lambda$startThread$0
#36 = Utf8 SourceFile
#37 = Utf8 Test.java
#38 = NameAndType #16:#17 // "":()V
#39 = Utf8 BootstrapMethods
#40 = MethodHandle #6:#59 // invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#41 = MethodType #17 // ()V
#42 = MethodHandle #7:#60 // invokespecial com/dafasoft/test/Test.lambda$startThread$0:()V
#43 = NameAndType #61:#62 // run:(Lcom/dafasoft/test/Test;)Ljava/lang/Runnable;
#44 = Utf8 java/lang/Thread
#45 = NameAndType #16:#63 // "":(Ljava/lang/Runnable;)V
#46 = NameAndType #64:#17 // start:()V
#47 = Class #65 // java/lang/System
#48 = NameAndType #66:#67 // out:Ljava/io/PrintStream;
#49 = Utf8 I am lambda of virtual
#50 = Class #68 // java/io/PrintStream
#51 = NameAndType #69:#70 // println:(Ljava/lang/String;)V
#52 = MethodHandle #6:#71 // invokestatic com/dafasoft/test/Test.lambda$main$1:()V
#53 = NameAndType #61:#72 // run:()Ljava/lang/Runnable;
#54 = Utf8 com/dafasoft/test/Test
#55 = NameAndType #23:#17 // startThread:()V
#56 = Utf8 I am lambda of static
#57 = NameAndType #28:#17 // showToast:()V
#58 = Utf8 java/lang/Object
#59 = Methodref #73.#74 // java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#60 = Methodref #10.#75 // com/dafasoft/test/Test.lambda$startThread$0:()V
#61 = Utf8 run
#62 = Utf8 (Lcom/dafasoft/test/Test;)Ljava/lang/Runnable;
#63 = Utf8 (Ljava/lang/Runnable;)V
#64 = Utf8 start
#65 = Utf8 java/lang/System
#66 = Utf8 out
#67 = Utf8 Ljava/io/PrintStream;
#68 = Utf8 java/io/PrintStream
#69 = Utf8 println
#70 = Utf8 (Ljava/lang/String;)V
#71 = Methodref #10.#76 // com/dafasoft/test/Test.lambda$main$1:()V
#72 = Utf8 ()Ljava/lang/Runnable;
#73 = Class #77 // java/lang/invoke/LambdaMetafactory
#74 = NameAndType #78:#82 // metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#75 = NameAndType #35:#17 // lambda$startThread$0:()V
#76 = NameAndType #34:#17 // lambda$main$1:()V
#77 = Utf8 java/lang/invoke/LambdaMetafactory
#78 = Utf8 metafactory
#79 = Class #84 // java/lang/invoke/MethodHandles$Lookup
#80 = Utf8 Lookup
#81 = Utf8 InnerClasses
#82 = Utf8 (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#83 = Class #85 // java/lang/invoke/MethodHandles
#84 = Utf8 java/lang/invoke/MethodHandles$Lookup
#85 = Utf8 java/lang/invoke/MethodHandles
{
public com.dafasoft.test.Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 5: 0
line 6: 4
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/dafasoft/test/Test;
private void startThread();
descriptor: ()V
flags: ACC_PRIVATE
Code:
stack=3, locals=3, args_size=1
0: aload_0
1: invokedynamic #2, 0 // InvokeDynamic #0:run:(Lcom/dafasoft/test/Test;)Ljava/lang/Runnable;
6: astore_1
7: new #3 // class java/lang/Thread
10: dup
11: aload_1
12: invokespecial #4 // Method java/lang/Thread."":(Ljava/lang/Runnable;)V
15: astore_2
16: aload_2
17: invokevirtual #5 // Method java/lang/Thread.start:()V
20: return
LineNumberTable:
line 9: 0
line 10: 7
line 11: 16
line 12: 20
LocalVariableTable:
Start Length Slot Name Signature
0 21 0 this Lcom/dafasoft/test/Test;
7 14 1 r Ljava/lang/Runnable;
16 5 2 t Ljava/lang/Thread;
private void showToast();
descriptor: ()V
flags: ACC_PRIVATE
Code:
stack=2, locals=1, args_size=1
0: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #7 // String I am lambda of virtual
5: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 15: 0
line 16: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/dafasoft/test/Test;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=4, args_size=1
0: invokedynamic #9, 0 // InvokeDynamic #1:run:()Ljava/lang/Runnable;
5: astore_1
6: new #3 // class java/lang/Thread
9: dup
10: aload_1
11: invokespecial #4 // Method java/lang/Thread."":(Ljava/lang/Runnable;)V
14: astore_2
15: aload_2
16: invokevirtual #5 // Method java/lang/Thread.start:()V
19: new #10 // class com/dafasoft/test/Test
22: dup
23: invokespecial #11 // Method "":()V
26: astore_3
27: aload_3
28: invokespecial #12 // Method startThread:()V
31: return
LineNumberTable:
line 19: 0
line 20: 6
line 21: 15
line 23: 19
line 24: 27
line 25: 31
LocalVariableTable:
Start Length Slot Name Signature
0 32 0 args [Ljava/lang/String;
6 26 1 r Ljava/lang/Runnable;
15 17 2 t Ljava/lang/Thread;
27 5 3 test Lcom/dafasoft/test/Test;
private static void lambda$main$1();
descriptor: ()V
flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #13 // String I am lambda of static
5: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 19: 0
private void lambda$startThread$0();
descriptor: ()V
flags: ACC_PRIVATE, ACC_SYNTHETIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #14 // Method showToast:()V
4: return
LineNumberTable:
line 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/dafasoft/test/Test;
}
SourceFile: "Test.java"
InnerClasses:
public static final #80= #79 of #83; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
0: #40 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#41 ()V
#42 invokespecial com/dafasoft/test/Test.lambda$startThread$0:()V
#41 ()V
1: #40 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;
Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;
Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#41 ()V
#52 invokestatic com/dafasoft/test/Test.lambda$main$1:()V
#41 ()V