目录 [−]
再了解了Java 8 Lambda的一些基本概念和应用后, 我们会有这样的一个问题: Lambda表达式被编译成了什么?。 这是一个有趣的问题,涉及到JDK的具体的实现。 本文将介绍OpenJDK对Lambda表达式的转换细节, 读者可以了解Java 8 Lambda表达式背景知识。
Lambda表达式的转换策略
Brian Goetz是Oracle的Java语言架构师, JSR 335(Lambda Expression)规范的lead, 写了几篇Lambda设计方面的文章, 其中之一就是Translation of Lambda Expressions。 这篇文章介绍了Java 8 Lambda设计时的考虑以及实现方法。
他提到, Lambda表达式可以通过内部类, method handle, dynamic proxy等方式实现, 但是这些方法各有优劣。 真正要实现Lambda表达式, 必须兼顾两个目标: 一是不引入特定策略,以期为将来的优化提供最大的灵活性, 二是保持类文件格式的稳定。 通过Java 7中引入的invokedynamic (JSR 292), 可以很好的兼顾这两个目标。
invokedynamic 在缺乏静态类型信息的情况下可以支持有效的灵活的方法调用。主要是为了日益增长的运行在JVM上的动态类型语言, 如Groovy, JRuby。
invokedynamic将Lambda表达式的转换策略推迟到运行时, 这也意味着我们现在编译的代码在将来的转换策略改变的情况下也能正常运行。
编译器在编译的时候, 会将Lambda表达式的表达式体 (lambda body)脱糖(desugar) 成一个方法,此方法的参数列表和返回类型和lambda表达式一致, 如果有捕获参数, 脱糖的方法的参数可能会更多一些, 并会产生一个invokedynamic调用, 调用一个call site。 这个call site被调用时会返回lambda表达式的目标类型(functional interface)的一个实现类。 这个call site称为这个lambda表达式的lambda factory。 lambda factory的bootstrap方法是一个标准方法, 叫做lambda metafactory。
编译器在转换lambda表达式时, 可以推断出表达式的参数类型,返回类型以及异常, 称之为natural signature
, 我们将目标类型的方法签名称之为lambda descriptor
, lambda factory的返回对象实现了函数式接口, 并且关联的表达式的代码逻辑, 称之为lambda object
。
转换举例
以上的解释有点晦涩, 简单来说
- 编译时
- Lambda 表达式会生成一个方法, 方法实现了表达式的代码逻辑
- 生成invokedynamic指令, 调用bootstrap方法, 由java.lang.invoke.LambdaMetafactory.metafactory方法实现
- 运行时
- invokedynamic指令调用metafactory方法。 它会返回一个CallSite, 此CallSite返回目标类型的一个匿名实现类, 此类关联编译时产生的方法
- lambda表达式调用时会调用匿名实现类关联的方法。
最简单的一个lambda表达式的例子:
|
|
使用javap查看生成的字节码 javap -c -p -v com/colobu/lambda/chapter5/Lambda1.class
:
|
|
可以看到, Lambda表达式体被生成一个称之为lambda$0
的方法。 看字节码知道它调用System.out.println输出传入的参数。
原lambda表达式处产生了一条invokedynamic #19, 0
。它会调用bootstrap
方法。
|
|
如果Lambda表达式写成Consumer<String> c = (Consumer<String> & Serializable)s -> System.out.println(s);
, 则BootstrapMethods的字节码为
|
|
它调用的是LambdaMetafactory.altMetafactory
,和上面的调用的方法不同。#114 1
意味着要实现Serializable
接口。
如果Lambda表达式写成,则BootstrapMethods的字节码为
|
|
#63 2
意味着要实现额外的接口。#64 1
意味着要实现额外的接口的数量为1。
字节码的指令含义可以参考这篇文章:Java bytecode instruction listings。
可以看到, Lambda表达式具体的转换是通过java.lang.invoke.LambdaMetafactory.metafactory实现的, 静态参数依照lambda表达式和目标类型不同而不同。
LambdaMetafactory.metafactory
现在我们可以重点关注以下 LambdaMetafactory.metafactory
的实现。
|
|
实际是由InnerClassLambdaMetafactory
的buildCallSite
来生成。 生成之前会调用validateMetafactoryArgs
方法校验目标类型(SAM)方法的参数/和产生的方法的参数/返回值类型是否一致。
metaFactory
方法的参数:
- caller: 由JVM提供的lookup context
- invokedName: JVM提供的NameAndType
- invokedType: JVM提供的期望的CallSite类型
- samMethodType: 函数式接口定义的方法的签名
- implMethod: 编译时产生的那个实现方法
- instantiatedMethodType: 强制的方法签名和返回类型, 一般和samMethodType相同或者是它的一个特例
上面的代码基本上是InnerClassLambdaMetafactory.buildCallSite
的包装,下面看看这个方法的实现:
|
|
其中spinInnerClass
调用asm
框架动态的产生SAM的实现类, 这个实现类的的方法将会调用编译时产生的那个实现方法。
你可以在编译的时候加上参数-Djdk.internal.lambda.dumpProxyClasses
, 这样编译的时候会自动产生运行时spinInnerClass
产生的类。
你可以访问OpenJDK的bug系统了解这个功能。 JDK-8023524
重复的lambda表达式
下面的代码中,在一个循环中重复生成调用lambda表达式,只会生成同一个lambda对象, 因为只有同一个invokedynamic
指令。
|
|
但是下面的代码会生成两个lambda对象, 因为它会生成两个invokedynamic
指令。
|
|
生成的类名
既然LambdaMetafactory会使用asm
框架生成一个匿名类, 那么这个类的类名有什么规律的。
|
|
输出结果如下:
|
|
类名格式如 <包名>.<类名>$$Lambda$
number是由一个计数器生成counter.incrementAndGet()。
后缀/<NN>
中的数字是一个hash值, 那就是类对象的hash值c.getClass().hashCode()
。
在Klass::external_name()
中生成。
|
|
直接调用生成的方法
上面提到, Lambda表达式体会由编译器生成一个方法,名字格式如Lambda$XXX
。
既然是类中的实实在在的方法,我们就可以直接调用。当然, 你在代码中直接写lambda$0()
编译通不过, 因为Lambda表达式体还没有被抽取成方法。
但是在运行中我们可以通过反射的方式调用。 下面的例子使用发射和MethodHandle两种方式调用这个方法。
|
|
捕获的变量等价于'final'
我们知道,在匿名类中调用外部的参数时,参数必须声明为final
。
Lambda体内也可以引用上下文中的变量,变量可以不声明成final
的,但是必须等价于final
。
下面的例子中变量capturedV等价与final
, 并没有在上下文中重新赋值。
|
|
如果反注释capturedV = null;
编译出错,因为capturedV在上下文中被改变。
但是如果反注释capturedV.greeting = "hi";
则没问题, 因为capturedV没有被重新赋值, 只是它指向的对象的属性有所变化。
方法引用
|
|
这段代码不会产生一个类似"Lambda$0"新方法。 因为LambdaMetafactory会直接使用这个引用的方法。
|
|
#59
指示实现方法为System.out::println