导读
Lambda表达式在过去几年中风靡编程世界。大多数现代语言都将它们作为函数式编程的基础部分。基于JVM的语言(如Scala,Groovy和Clojure)已将它们集成为语言的关键部分,本文将会对比Java与Scala在编译Lambda表达式上存在的差异
Lambda表达式在过去几年中风靡编程世界。大多数现代语言都将它们作为函数式编程的基础部分。基于JVM的语言(如Scala,Groovy和Clojure)已将它们集成为语言的关键部分,Java自然也不甘落后。本文并不是教大家如何书写Lambda表达式(如果这点基础还不会,自行阅读《疯狂Java讲义》吧)。
Lambda表达式的有趣之处在于,从JVM的角度来看,它们是完全不可见的。它没有匿名函数或Lambda表达式的概念。它只知道字节码是严格的OO规范。由语言及其编译器的制造商在这些约束下工作以创建更新,更高级的语言元素。
我们一起来看看Scala和Java编译器如何实现Lambda表达式会很有趣。结果非常令人惊讶。
为了实现这一目标,我采用了一个简单的Lambda表达式,将一个字符串列表转换为它们的长度列表。
Java代码:
代码语言:javascript复制List<String> names = Arrays.asList("1", "2", "3");
Stream lengths = names.stream().map(name -> name.length());
Scala代码
代码语言:javascript复制val names = List("1", "2", "3")
val lengths = names.map(name => name.length)
不要被它的简单性所欺骗,简单代码的背后发生了一些复杂的事情。
从Scala开始吧
01
编译后的代码
我使用javap来查看Scala编译器生成的.class的字节码内容。让我们看一下结果字节码(这就是JVM实际执行的内容)。
代码语言:javascript复制// 这行代码把names变量加载到栈里面(JVM把它当成#2变量)
// 该变量将会在此处停留一会,直到map函数来“消费”它。
aload_2
接下来,事情变得更有趣了—— 创建并初始化由编译器生成的合成类的新实例。从JVM的角度来看,这是一个拥有Lambda方法的对象。有趣的是,虽然Lambda被定义为我们方法的一个组成部分,但实际上它完全存在于我们的课程之外。
代码语言:javascript复制new myLambdas/Lambda1$$anonfun$1 // 实例化Lambda对象
dup // 把它再次放入栈中
// 最后,调用 c’tor. 记住:从JVM的角度来看,它只是一个普通对象
invokespecial myLambdas/Lambda1$$anonfun$1/()V
// 这两行长的代码加载了immutable.List CanBuildFrom工厂(它负责创建新的list),
// 工厂模式是Scala集合体系的一部分
getstatic scala/collection/immutable/List$/MODULE$
Lscala/collection/immutable/List$;
invokevirtual scala/collection/immutable/List$/canBuildFrom()
Lscala/collection/generic/CanBuildFrom;
// 现在我们在栈里面有Lambda对象和工厂。
// 下一步是调用map()函数
// 如果你还记得,我们在开始时将names变量加载到栈中
// 现在它将被作为this来调用map()函数
// 它将接受该Lambda对象和工厂、用于来生成一个新的列表。
invokevirtual scala/collection/immutable/List/map(Lscala/Function1;
Lscala/collection/generic/CanBuildFrom;)Ljava/lang/Object;
请注意看: Lambda对象内部发生了什么?
02
Lambda对象
Lambda类派生自scala.runtime.AbstractFunction1。通过这个,map()函数可以多态调用覆盖的apply(),其代码如下
代码语言:javascript复制// 这段代码加载了这个以及要操作的目标对象,
// 检查它是否为String,然后调用另一个apply()方法来执行实际工作
// 最后将其返回值自动装箱后再返回
aload_0 // 加载this
aload_1 // 加载string参数
checkcast java/lang/String // 保证它是一个字符串——我们得到的是Object
// 在合成类中调用另一个apply()方法
invokevirtual myLambdas/Lambda1$$anonfun$1/apply(Ljava/lang/String;)I
// 对结果自动装箱
invokestatic scala/runtime/BoxesRunTime/boxToInteger(I)Ljava/lang/Integer
areturn
执行.length()操作的实际代码嵌套在另一个apply方法中,该方法只返回我们预期的String的长度。
到达这里有很长的路要走!
代码语言:javascript复制aload_1
invokevirtual java/lang/String/length()I
ireturn
对于像我们上面写的那样简单的一行,生成了很多字节码 - 一个额外的类和一堆新方法。这当然不是为了阻止我们使用Lambda(我们用Scala编写,而不是C语言)。它只是展示了这些结构背后的复杂性。想一想编译复杂的Lambda表达式链的代码量和复杂性!
Java——一种新的解决方案
01
编译后的代码
这里的字节码有点短,但确实令人惊讶。它开始很简单,只需加载names变量,并调用它的.stream()方法,但它会做一些相当优雅的事情。它并没有创建包装Lambda函数的新对象,而是使用Java 7新引进的invokeDynamic指令将此调用点动态链接到实际的Lambda函数。
代码语言:javascript复制aload_1 // 加载names变量
// 调用它的stream()函数
invokeinterface java/util/List.stream:()Ljava/util/stream/Stream;
//invokeDynamic magic!
invokedynamic #0:apply:()Ljava/util/function/Function;
// 调用它的map()函数
invokeinterface java/util/stream/Stream.map:
(Ljava/util/function/Function;)Ljava/util/stream/Stream;
InvokeDynamic魔术:在Java 7中添加了此JVM指令,这样使得JVM不那么严格,并允许动态语言在运行时绑定符号,而不是在JVM编译代码时静态地执行所有链接。
动态链接:如果查看实际的invokedynamic指令,你将看到没有实际Lambda函数的引用(称为lambda$0)。答案在于invokedynamic的设计方式(该指令的设计非常优雅,下次我们专门写一篇文章来介绍该指令),简单来说,就在于Lambda的名称和签名,在我们的例子中有如下代码:
代码语言:javascript复制// lambda$0函数获取一个String、返回一个Integer
lambdas/Lambda1.lambda$0:(Ljava/lang/String;)Ljava/lang/Integer;
存储在.class中单独的表中的条目中,#0参数传递给指令点。这个新表实际上在几年后第一次改变了字节码规范的结构,这要求我们也将Takipi的错误分析引擎改编成它。
02
Lambda代码
这是实际Lambda表达式的代码。这是非常棒的切割器——只需加载String参数,调用length()并将结果打包。请注意,它被编译为静态函数,以避免像我们在Scala中看到的那样将其他对象传递给它。
代码语言:javascript复制aload_0
invokevirtual java/lang/String.length:()
invokestatic java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
areturn
这是invokedynamic方法的另一个优点,因为它允许我们从.map()函数的角度以多态方式调用方法,但不必分配包装器对象或调用虚拟覆盖方法。太酷了!
总结
看到现代语言中最“严格”的Java如何使用动态链接为其新的Lambda表达式提供动力,这真是令人着迷。它也是一种有效的方法,因为不需要额外的类加载和编译 - Lambda方法只是我们类中的另一个私有方法。
Java通过Java 7中引入的新技术、然后用非常简单的方式实现Lambda表达式,而且实现得非常优雅。通过研究代码背后的运作机制,可以让人获得更多乐趣。
本文结束