首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java与Scala如何编译Lambda表达式?

Java与Scala如何编译Lambda表达式?

作者头像
疯狂软件李刚
发布2020-06-24 16:59:27
1.3K0
发布2020-06-24 16:59:27
举报
文章被收录于专栏:疯狂软件李刚疯狂软件李刚

导读

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代码:

List<String> names = Arrays.asList("1", "2", "3");
Stream lengths = names.stream().map(name -> name.length());

Scala代码

val names = List("1", "2", "3")
val lengths = names.map(name => name.length)

不要被它的简单性所欺骗,简单代码的背后发生了一些复杂的事情。

从Scala开始吧

01

编译后的代码

我使用javap来查看Scala编译器生成的.class的字节码内容。让我们看一下结果字节码(这就是JVM实际执行的内容)。

// 这行代码把names变量加载到栈里面(JVM把它当成#2变量)
// 该变量将会在此处停留一会,直到map函数来“消费”它。
 aload_2

接下来,事情变得更有趣了—— 创建并初始化由编译器生成的合成类的新实例。从JVM的角度来看,这是一个拥有Lambda方法的对象。有趣的是,虽然Lambda被定义为我们方法的一个组成部分,但实际上它完全存在于我们的课程之外。

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(),其代码如下

// 这段代码加载了这个以及要操作的目标对象,
// 检查它是否为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的长度。

到达这里有很长的路要走!

aload_1

invokevirtual java/lang/String/length()I
ireturn

对于像我们上面写的那样简单的一行,生成了很多字节码 - 一个额外的类和一堆新方法。这当然不是为了阻止我们使用Lambda(我们用Scala编写,而不是C语言)。它只是展示了这些结构背后的复杂性。想一想编译复杂的Lambda表达式链的代码量和复杂性!

Java——一种新的解决方案

01

编译后的代码

这里的字节码有点短,但确实令人惊讶。它开始很简单,只需加载names变量,并调用它的.stream()方法,但它会做一些相当优雅的事情。它并没有创建包装Lambda函数的新对象,而是使用Java 7新引进的invokeDynamic指令将此调用点动态链接到实际的Lambda函数。

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的名称和签名,在我们的例子中有如下代码:

// lambda$0函数获取一个String、返回一个Integer
lambdas/Lambda1.lambda$0:(Ljava/lang/String;)Ljava/lang/Integer;

存储在.class中单独的表中的条目中,#0参数传递给指令点。这个新表实际上在几年后第一次改变了字节码规范的结构,这要求我们也将Takipi的错误分析引擎改编成它。

02

Lambda代码

这是实际Lambda表达式的代码。这是非常棒的切割器——只需加载String参数,调用length()并将结果打包。请注意,它被编译为静态函数,以避免像我们在Scala中看到的那样将其他对象传递给它。

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表达式,而且实现得非常优雅。通过研究代码背后的运作机制,可以让人获得更多乐趣。

本文结束

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-05-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 疯狂软件李刚 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档