1、概述
java8推出lambda表达式可谓是java非常大的一个特性,java的使用本文不探讨。
本文记录在探索lambda实现的过程,假设有下面一段lambda的代码:
1 |
|
从上面的代码可以看到myLambda
方法形参需要一个Predicate
函数接口,于是在main方法中实现的时候就传入了一个lambda表达式(6~9行),代码很简单,最终会输出:1
2
3myLambda表达式
main方法中的lambda表达式(Predicate)
true
接下来,我们开始探索….
2、lambda表达式编译成class发生了什么?
2.1、匿名lambda
将上面的代码编译,并反编译查看其class字节码(使用javap或者用jclasslib工具软件查看都行),字节码文件如下:

可以看到,在方法表中,有一个签名为:0x100a [private static synthetic]
的方法lambda$main$0
,该方法的形参和返回值分别是 java.lang.String
和boolean
类型,巧了,这个方法不就是Predicate<String>
的test
方法形参和返回值吗?如下:

这个方法就是由我们的lambda表达式自动生成的,通过synthetic
这个单词我们也大致能看出端倪——这个单词的意思是:“合成”,因此,我们可以大胆的猜想,最终JVM虚拟机在执行的时候,其实就是调用的这个lambda$main$0
方法。
【补充:通过实践,发现这个方法的生成是有模板的lambda$methodName$index
:
- 其中
lambda$
是固定的, - 紧接着就是一个方法名,这个方法名就是用到了lambda表达式的方法名称,假设上面的代码是main函数中使用了lambda表达式,那么紧接着就是main,
- 最后一个是 序号,表示当前方法中的第几个lambda表达式】
2.2、方法引用
我们在使用lambda表达式时,除了使用匿名函数的方式,还可以使用方法引用,例如:
1 | import java.util.function.Predicate; |
我们再来看看class字节码将会是怎样,如下:

可以看到,没有生成lambda$
开头的方法了,说明:如果我们使用匿名lambda表达式,java编译器会自动为我们合成一种方法,如果是方法引用,java编译器就直接引用我们指定的方法了,不会合成方法。
3、JVM底层最终是怎么执行的呢?
3.1、需要关注的问题
通过第2节的探索,我们知道:我们写的lambda表达式的代码最终要么是编译器自动给我们生成了一个方法,要么就是直接指向了一个现有的方法。
但是现在问题来了,从上面的代码可以看出:调用myLambda
方法需要传入一个Predicate
接口的对象,我们现在知道当前类中自动或手动指向了一个方法,而这个方法最终归属的Lanting
这个类并不实现Predicate
这个接口呀,那最终JVM最终调用myLambda
这个方法时,那个Predicate
对象是怎么生成的呢?
带着问题,我们接着探索
3.2、invokedynamic指令
我们查看字节码,找到Lanting
这个类的main
方法的底层指令,如下图:

上图里有个很关键的指令invokedynamic
,这个指令指向了一个符号引用,这个符号引用如下图:

通过这个符号引用我们可以看到,这是一个invokedynamic
信息,这个信息分为两部分,一部分是 名字和描述符,一部分是 Bootstrap方法
我们先看名字和描述符:
在我们这个例子中,名字描述符为内容为<test : ()Ljava/util/function/Predicate;>
,这个名字和描述符大致说明了:这是一个方法,这个方法没有传入参数,返回值是一个Predicate
实例,并且,Predicate
实例有个动态调用点,这个动态调用点是test
方法。
总结下来就是:invokedynamic指令实际调用的是一个方法,那个方法需要返回一个Predicate实例,并且在Predicate实例中的test方法为动态调用(可以理解为一个切面捕获点,调用到test方法后,会转向去执行另一个绑定的方法)。
我们再来看Bootstrap方法,Bootstrap方法符号引用的详情如下图:

可以看到,这个Bootstrap方法实际对应的是一个invoke包中的一个静态方法java/lang/invoke/LambdaMetafactory.metafactory
,OK,那我们现在就去看看,其源代码如下:

哇欧,还真有这个方法,那怎么验证确实是调了这个方法呢?我们在这个方法里打个断点,再以debug模式执行上面的代码,发现,真的就停在了这里!!!!!

我们来看看这个方法的几个参数:
- caller
这是代表的调用方的上下文(获取方式很固定:MethodHandles.lookup()
) - invokedName
这是lambda中实际需要实现的接口的方法名【这里说成动态调用点方法更为合适,这个方法名就是需要转嫁到其他方法的一个“捕获点”方法,调到这个方法的时候,就会转嫁到lambda的那个方法引用上】,例如,本实例中的名字就是test
,因为Predicate中需要实现的方法就是这个。
【补充,这个方法可以是任意方法,只要是接口中有的,比如甚至可以是equals,不一定必须接口中的抽象方法】 - invokedType
这是一个方法描述,也就是刚刚说的“名字和描述符”,本例中是<test : ()Ljava/util/function/Predicate;>
,他就是invokedynamic指令对应的那个方法描述,主要用于生成内部类。 - samMethodType
这是invokedName方法对应的方法描述,也就是记录 invokedName 方法的形参和返回值 - implMethod
lambda中具体实现的方法引用,也就是最终会调用的方法 - instantiatedMethodType
implMethod方法的方法描述。
这里看不懂没关系,我后文会写个使用java去实现动态调用的示例。
回到刚刚Bootstrap方法的描述,也许你会发现,他的参数只有三个,这是为什么呢?是因为文档中写了caller
、invokedName
、invokedType
这前三个字段是JVM自动压栈填充进去的,所以,只需要传入后三个参数即可。
metafactory
方法的作用就是生成一个动态调用点——CallSite
,通过这个CallSite,我们可以拿到动态生成的那个内部类(这个例子里的Predicate的实例对象)
最后,如果这个CallSite的“捕获方法”被调用后,会转到我们lambda中的方法引用上去。
3.3、总结
- lambda表达式会在底层使用一个invokedynamic指令
- invokedynamic指令依赖
java/lang/invoke/LambdaMetafactory.metafactory
去动态生成一个接口实例 - 接口实例中会有一个方法,该方法实际是一个“动态调用点”方法,调用到该方法时,实际会通过MethodHandle去调用到我们的方法引用指向的那个方法
4、使用java去模拟动态调用
废话不多说,先上代码:
1 |
|
可以看到,上面的代码,我先创建了一个方法引用md ,即:Main.class中的形参有一个字符串,返回值为boolean的方法。
紧接着,调用了LambdaMetafactory类的静态方法metafactory,传入了几个参数,第一个参数Lookup固定写法,第二个到第四个参数分别是指的待实现的接口的方法名、用于生成接口实例的方法描述、接口的方法描述(形参与返回值),第5个和第6个参数分别是方法引用(最终需要调用到的那个方法)与方法的描述符。
执行上面的代码,输出结果是:
method myPredicate 哈哈哈
说明,myPredicate方法最终被调用到了,并且也收到了 “哈哈哈” 这个实参。
实验,我们把上面的代码改成这样(也就是把test方法改成 equals方法),可以吗?
是可以的!即使 test这个抽象方法没被实现。
1 |
|