最近几年 Lambda 表达式风靡于编程界. 很多现代编程语言都把它作为函数式编程的基本组成部分.
基于 JVM 的编程语言如 Scala,Groovy 还有 Clojure 把它们作为关键部分集成在语言中. 现在 Java8 也加入了它们的行列.
有趣的是, 对于 JVM 来说,Lambda 表达式是完全不可见的, 并没有匿名函数和 Lamada 表达式的概念, 它只知道字节码是严格面向对象规范的. 它取决于语言的作者和它的编译器在规范限制内创造出更新, 更高级的语言元素.
我们第一次接触它是在我们要给 Takipi 添加 Scala 支持的时候, 我们不得不深入研究 Scala 的编译器. 伴随着 JAVA8 的来临, 我认为探究 Scala 和 java 编译器是如何实现 Lambda 表达式是非常有趣的事情. 结果也是相当出人意料.
接下来,我展示一个简单的 Lambda 表达式,用于将字符串集合转化成字符串自身长度的集合。
Java 的写法 –
1List names = Arrays.asList(“1”, “2”, “3”);
2Stream lengths = names.stream().map(name -> name.length());
Scala 的写法 –
1.val names = List(“1”, “2”, “3”)
2.val lengths = names.map(name =>name.length)
表面上看起来非常简单,那么后面的复杂东西是怎么搞的呢?
一起分析 Scala 的实现方式
The Code
我使用 javap(jdk 自带的工具)去查看 Scala 编译器编译出来的 class 类中所包含的字节码内容。让我们一起看看最终的字节码(这是 JVM 将真正执行的)
1.// 加载 names 对象引用, 压入操作栈 (JVM 把它当成变量 #2)
2.// 它将停留一会, 直到被 map 函数调用.
3.aload_2
接下来的东西变得更加有趣了,编译器产生的一个合成类的实例被创建和初始化。从 JVM 角度,就是通过这个对象持有 Lambda 方法的。有趣的是虽然 Lambda 被定义为我们方法的一个组成部分,但实际上它完全存在于我们的类之外。
new myLambdas/Lambda1$$anonfun$1 //new 一个 lambda 实例变量.
dup // 把 lambda 实例变量引用压入操作栈.// 最后, 调用它的构造方法. 记住, 对于 JVM 来说, 它仅仅只是一个普通对象.
invokespecial myLambdas/Lambda1$$anonfun$1/()V// 这两行长的代码加载了用于创建 list 的 immutable.List CanBuildFrom 工厂。
// 这个工厂模式是 Scala 集合架构的一部分。
getstatic scala/collection/immutable/List$/MODULE$
Lscala/collection/immutable/List$;
invokevirtual scala/collection/immutable/List$/canBuildFrom()
Lscala/collection/generic/CanBuildFrom;// 现在我们的操作栈中已经有了 Lambda 对象和工厂
// 接下来的步骤是调用 map 函数。
// 如果你记得,我们一开始已经将 names 对象引用压入操作栈顶。
// names 对象现在被作为 map 方法调用的实例,
// 它也可以接受 Lambda 对象和工厂用于生成一个包含字符串长度的新集合。
invokevirtual scala/collection/immutable/List/map(Lscala/Function1;
Lscala/collection/generic/CanBuildFrom;)Ljava/lang/Object;
但是,等等,Lambda 对象内部到底发生了什么呢?
Lambda 对象
Lambda 类衍生自 scala.runtime.AbstractFunction1。通过调用 map 函数可以多态调用被重写的 apply 方法,被重写的 apply 方法代码如下:
aload_0 // 加载 this 对象引用到操作栈
aload_1 // 加载字符串参数到操作栈
checkcast java/lang/String // 检查是不是字符串类型// 调用合成类中重写的 apply 方法
invokevirtual myLambdas/Lambda1$$anonfun$1/apply(Ljava/lang/String;)I// 包装返回值
invokestatic scala/runtime/BoxesRunTime/boxToInteger(I)Ljava/lang/Integer
areturn
真正用于执行 length() 操作的代码被嵌套在额外的 apply 方法中,用于简单的返回我们所期望的字符串长度。
我们前面走了一段很长的路,终于到这边了:
aload_1
invokevirtual java/lang/String/length()I
ireturn
对于我们上面写的简单的代码,最后生成了大量的字节码,一个额外的类和一堆新的方法。当然,这并不意味着会让我们放弃使用 Lambda(我们是在写 scala,不是 C)。这仅仅表明了这些结构后面的复杂性. 试想 Lambda 表达式的代码和复杂的东西将被编译成复杂的执行链。
我预计 Java8 会以相同的方式实现 Lambda,但出人意料的是,他们使用了另一种完全不同的方式。
Java 8 – 新的实现方式
Java8 的实现, 字节码比较短,但是做的事情却很意外。它一开始很简单地加载 names 变量, 并且调用它的 stream 方法, 但它接下来做的东东就显得很优雅了. 它使用一个 Java7 加入的一个新指令 invokeDynamic 去动态地连接 lambda 函数的真正调用点, 从而代替创建一个用于包装 lambda 函数的对象.
aload_1 // 加载 names 对象引用,压入操作栈
// 调用它的 stream() 方法
invokeinterface java/util/List.stream:()Ljava/util/stream/Stream;// 神奇的 invokeDynamic 指令!
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 限制少了, 并且允许动态语言运行时绑定符号.
动态链接. 如果你看到 invokedynamic 指令, 你会发现实际上没有任何 Lambda 函数的引用 (名为 lambda$0),这是因为 invokedynamic 的设计方式,简单地说就是 lambda 的名称和签名,如我们的例子 -
// 一个名为 Lamda$0 的方法,获得一个字符串参数并返回一个 Integer 对象
lambdas/Lambda1.lambda$0:(Ljava/lang/String;)Ljava/lang/Integer;
他们保存在.class 文件中一个单独的表的条目中,执行 invokedynamic 时会将 #0 参数传给指令指针。这个新的表的确在很多年后的今天首次改变了字节码规范的结构,这也就需要我们改编 Takipi 的错误分析引擎来配合。
The Lambda code
下面这个字节码是真正的 lambda 表达式. 然后就是千篇一律地、简单地加载字符串参数, 调用 length 方法获得长度, 并且包装返回值. 注意它是作为静态方法编译的, 从而避免了传递一个额外的 this 对象给他, 就像我们前面看到的 Scala 中的做法.
aload_0
invokevirtual java/lang/String.length:()
invokestatic java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
areturn
invokedynamic 方式的另一个优点是, 它允许我们使用 map 函数多态地调用这个方法, 而不需要去实例化一个封装对象或调用重写的方法. 非常酷吧!
总结:探究 java, 这个最严格的的现代编程语言是如何使用动态连接加强它的 lambda 表达式是非常吸引人的事情. 这是一个非常高效的方式, 不需要额外的类加载, 也不需要编译,Lambda 方法是我们类中的另一个简单的私有方法.
Java 8 使用 Java 7 中引入的新技术,使用一个非常直接的方式实现了 Lambda 表达式,干得非常漂亮。像 java 这样”端庄”的淑女也可以教我们一些新的花样真是非常让人高兴。