天天看點

編譯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