Java8有两大新特性,一个是Lambda表达式,第二个是Stream API。虽然平时在工作中写起来挺爽的,但是一直不理解底层是怎么运行起来的,所以本文尝试着分析一波Lambda表达式,如果有说的不对的地方,请各位帮忙指正一下呀!
Lambda表达式介绍:
可以简单理解为传递匿名函数的一种方式,它没有名称,只有参数列表,函数主体,返回类型,可能还有可以抛出的异常列表等等。个人理解它重要用法是简化匿名内部类的代码编写。例如:
两者是等价的,可以看出来下面这种使用了Lambda之后,代码要少很多!Java8中的Lambda的基本语法如下所示:
(parameters) -> expression
(parameters) -> { statements; }
但是要注意的是,并非所有的匿名内部类都可以写成这种形式,只有当它符合函数式接口(接口上有@FunctionalInterface标记),才可以使用Lambda表达式。
那什么又是函数式接口呢?
所谓的函数式接口其实就是只定义一个抽象方法的接口,函数式接口出现的地方都可以将代码简写成Lambda表达式,例如上面举例的Runnable:
这个抽象方法被称为函数描述符,例如上面的这个run(),它描述的就是这个接口什么参数也不接收,什么参数也不反悔。你编写的代码如果不符合这个描述会报错,报错是由编译器做的。
Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例。
Java8中定义了多种函数式接口:
有很多,这里举几个例子说明下,前者为接口名称,后者为它的描述符:
Predicete
Consumer
Function
……(其他的可见“参考二”,总结的挺全面的)
每种接口都定义了不同的函数描述符。了解了以上内容之后,我们再来看下Lambda表达式最终是怎么被执行的呢。
Lambda表达式最终是怎么被执行的呢?
使用 javap -v -p .\Tets.class 反编译文件,查看字节码信息,这里截取了曲中的一部分内容:
4: invokedynamic #3, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
private static void lambda$main$0();
descriptor: ()V
flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #11 // String hello
5: invokevirtual #12 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 8: 0
}
SourceFile: "Tets.java"
InnerClasses:
public static final #83= #82 of #87; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
0: #36 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:
#37 ()V
#38 invokestatic Tets.lambda$main$0:()V
#37 ()V
1: #36 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:
#45 (Ljava/lang/Object;)V
#46 invokevirtual java/io/PrintStream.print:(Ljava/lang/String;)V
#47 (Ljava/lang/String;)V
可以看到,Lambda表达式所在处会产生一个invokedynamic的指令,在之前的《JVM是如何实现方法调用的》曾经提到过Invokedynamic指令:
其他四中调用方法的指令是把符号引用替换为直接引用,直接指到内存中的地址,也就是说他们需要指导方法所在类名,方法名以及方法描述符。但是像Lambda表达式这种,只要方法描述符对上了,可以使用引用到其他类中的方法(任意Function类中的apply)。Invokedynamic就是这么一个指令,允许程序将调用点连接到任意符合条件的方法上。
除了这个指令之外,还生成了一个Bootstrap Method方法,Bootstrap Method是java.lang.invoke.LambdaMetafactory中的一个静态方法,它返回一个CallSite对象。从#38可以看出,这个方法的第二个入参就是我们编写的Lambda表达式中的内容,即这样CallSite就和Lambda表达式联系在一起了。
总结一下就是:
1.Lambda表达的内容被编译成了当前类的一个静态或实例方法.
2.Lambda表达式所在处会产生一条invokedynamic指令调用, 同时编译器会生成一个对应的Bootstrap Method.
3.当JVM第一次碰到这条invokedynamic时, 会调用对应的Bootstrap方法.
4.由Lambda表达式产生的invokedynamic指令的引导方法是调用LambdaMetafactory.metafactory()方法.
5.调用引导方法会返回一个CallSite对象实例
6.执行MethodHanlde代表的方法, 返回结果, 结果为动态生成的接口实例, 接口实现调用“1”中生成的方法.
参考:
《Java8实战》
https://blog.csdn.net/lz710117239/article/details/76192629(Java8种的函数式接口)
https://www.cnblogs.com/jqqiang/p/7482823.html(内联函数)
https://blog.csdn.net/hj7jay/article/details/73480386(方法句柄与反射速度比较)
https://www.oracle.com/technetwork/java/jvmls2013kuksen-2014088.pdf(Lambda性能测试报告)
https://www.2cto.com/kf/201604/502490.html(Lambda底层字节码分析)