天天看点

编译Lambda表达式: Scala和Java 8

编译Lambda表达式: Scala和Java 8

最近几年lambda表达式风靡于编程界. 很多现代编程语言都把它作为函数式编程的基本组成部分. 基于jvm的编程语言如scala,groovy还有clojure把它们作为关键部分集成在语言中.现在java8也加入了它们的行列.

有趣的是,对于jvm来说,lambda表达式是完全不可见的,并没有匿名函数和lamada表达式的概念,它只知道字节码是严格面向对象规范的.它取决于语言的作者和它的编译器在规范限制内创造出更新,更高级的语言元素.

我们第一次接触它是在我们要给takipi添加scala支持的时候, 我们不得不深入研究scala的编译器.伴随着java8的来临,我认为探究scala和java编译器是如何实现lambda表达式是非常有趣的事情.结果也是相当出人意料.

接下来,我展示一个简单的lambda表达式,用于将字符串集合转化成字符串自身长度的集合。

java的写法 –

<code>1</code>

<code>list names = arrays.aslist(</code><code>"1"</code><code>,</code><code>"2"</code><code>,</code><code>"3"</code><code>);</code>

<code>2</code>

<code>stream lengths = names.stream().map(name -&gt; name.length());</code>

scala的写法 –

<code>val names = list(</code><code>"1"</code><code>,</code><code>"2"</code><code>,</code><code>"3"</code><code>)</code>

<code>val lengths = names.map(name =&gt;name.length)</code>

表面上看起来非常简单,那么后面的复杂东西是怎么搞的呢?

编译Lambda表达式: Scala和Java 8

我使用javap(jdk自带的工具)去查看scala编译器编译出来的class类中所包含的字节码内容。让我们一起看看最终的字节码(这是jvm将真正执行的)

<code>// 加载names对象引用,压入操作栈(jvm把它当成变量#2)</code>

<code>// 它将停留一会,直到被map函数调用.</code>

<code>3</code>

<code>aload_2</code>

接下来的东西变得更加有趣了,编译器产生的一个合成类的实例被创建和初始化。从jvm角度,就是通过这个对象持有lambda方法的。有趣的是虽然lambda被定义为我们方法的一个组成部分,但实际上它完全存在于我们的类之外。

<code>01</code>

<code>new</code> <code>mylambdas/lambda1$$anonfun$</code><code>1</code> <code>//new一个lambda实例变量.</code>

<code>02</code>

<code>dup</code><code>//把lambda实例变量引用压入操作栈.</code>

<code>03</code>

<code>04</code>

<code>// 最后,调用它的构造方法.记住,对于jvm来说,它仅仅只是一个普通对象.</code>

<code>05</code>

<code>invokespecial mylambdas/lambda1$$anonfun$</code><code>1</code><code>/()v</code>

<code>06</code>

<code>07</code>

<code>//这两行长的代码加载了用于创建list的immutable.list canbuildfrom工厂。</code>

<code>08</code>

<code>//这个工厂模式是scala集合架构的一部分。</code>

<code>09</code>

<code>getstatic scala/collection/immutable/list$/module$</code>

<code>10</code>

<code>lscala/collection/immutable/list$;</code>

<code>11</code>

<code>invokevirtual scala/collection/immutable/list$/canbuildfrom()</code>

<code>12</code>

<code>lscala/collection/generic/canbuildfrom;</code>

<code>13</code>

<code>14</code>

<code>// 现在我们的操作栈中已经有了lambda对象和工厂</code>

<code>15</code>

<code>// 接下来的步骤是调用map函数。</code>

<code>16</code>

<code>// 如果你记得,我们一开始已经将names对象引用压入操作栈顶。</code>

<code>17</code>

<code>// names对象现在被作为map方法调用的实例,</code>

<code>18</code>

<code>// 它也可以接受lambda对象和工厂用于生成一个包含字符串长度的新集合。</code>

<code>19</code>

<code>invokevirtual scala/collection/immutable/list/map(lscala/function1;</code>

<code>20</code>

<code>lscala/collection/generic/canbuildfrom;)ljava/lang/object;</code>

但是,等等,lambda对象内部到底发生了什么呢?

lambda类衍生自scala.runtime.abstractfunction1。通过调用map函数可以多态调用被重写的apply方法,被重写的apply方法代码如下:

<code>aload_0</code><code>//加载this对象引用到操作栈</code>

<code>aload_1</code><code>//加载字符串参数到操作栈</code>

<code>checkcast java/lang/string</code><code>//检查是不是字符串类型</code>

<code>// 调用合成类中重写的apply方法</code>

<code>invokevirtual mylambdas/lambda1$$anonfun$</code><code>1</code><code>/apply(ljava/lang/string;)i</code>

<code>//包装返回值</code>

<code>invokestatic scala/runtime/boxesruntime/boxtointeger(i)ljava/lang/integer</code>

<code>areturn</code>

真正用于执行length()操作的代码被嵌套在额外的apply方法中,用于简单的返回我们所期望的字符串长度。

我们前面走了一段很长的路,终于到这边了:

<code>aload_1</code>

<code>invokevirtual java/lang/string/length()i</code>

<code>ireturn</code>

对于我们上面写的简单的代码,最后生成了大量的字节码,一个额外的类和一堆新的方法。当然,这并不意味着会让我们放弃使用lambda(我们是在写scala,不是c)。这仅仅表明了这些结构后面的复杂性.试想lambda表达式的代码和复杂的东西将被编译成复杂的执行链。

我预计java8会以相同的方式实现lambda,但出人意料的是,他们使用了另一种完全不同的方式。

编译Lambda表达式: Scala和Java 8

java8的实现,字节码比较短,但是做的事情却很意外。它一开始很简单地加载names变量,并且调用它的stream方法,但它接下来做的东东就显得很优雅了.它使用一个java7加入的一个新指令invokedynamic去动态地连接lambda函数的真正调用点,从而代替创建一个用于包装lambda函数的对象.

<code>aload_1</code><code>//加载names对象引用,压入操作栈</code>

<code>//调用它的stream()方法</code>

<code>invokeinterface java/util/list.stream:()ljava/util/stream/stream;</code>

<code>//神奇的invokedynamic指令!</code>

<code>invokedynamic #</code><code>0</code><code>:apply:()ljava/util/function/function;</code>

<code>//调用map方法</code>

<code>invokeinterface java/util/stream/stream.map:</code>

<code>(ljava/util/function/function;)ljava/util/stream/stream;</code>

神奇的invokedynamic指令. 这个是java 7新加入的指令,它使得jvm限制少了,并且允许动态语言运行时绑定符号.

动态链接. 如果你看到invokedynamic指令,你会发现实际上没有任何lambda函数的引用(名为lambda$0),这是因为invokedynamic的设计方式,简单地说就是lambda的名称和签名,如我们的例子-

<code>// 一个名为lamda$0的方法,获得一个字符串参数并返回一个integer对象</code>

<code>lambdas/lambda1.lambda$</code><code>0</code><code>:(ljava/lang/string;)ljava/lang/integer;</code>

下面这个字节码是真正的lambda表达式.然后就是千篇一律地、简单地加载字符串参数,调用length方法获得长度,并且包装返回值.注意它是作为静态方法编译的,从而避免了传递一个额外的this对象给他,就像我们前面看到的scala中的做法.

<code>aload_0</code>

<code>invokevirtual java/lang/string.length:()</code>

<code>invokestatic java/lang/integer.valueof:(i)ljava/lang/integer;</code>

<code>4</code>

invokedynamic 方式的另一个优点是,它允许我们使用map函数多态地调用这个方法,而不需要去实例化一个封装对象或调用重写的方法.非常酷吧!

总结:探究java,这个最严格的的现代编程语言是如何使用动态连接加强它的lambda表达式是非常吸引人的事情.这是一个非常高效的方式,不需要额外的类加载,也不需要编译,lambda方法是我们类中的另一个简单的私有方法.

java 8 使用java 7中引入的新技术,使用一个非常直接的方式实现了lambda表达式,干得非常漂亮。像java这样”端庄”的淑女也可以教我们一些新的花样真是非常让人高兴。

编译Lambda表达式: Scala和Java 8