編譯Lambda表示式: Scala和Java 8
最近幾年Lambda表示式風靡於程式設計界. 很多現代程式語言都把它作為函數語言程式設計的基本組成部分. 基於JVM的程式語言如Scala,Groovy還有Clojure把它們作為關鍵部分整合在語言中.現在Java8也加入了它們的行列.
有趣的是,對於JVM來說,Lambda表示式是完全不可見的,並沒有匿名函式和Lamada表示式的概念,它只知道位元組碼是嚴格面向物件規範的.它取決於語言的作者和它的編譯器在規範限制內創造出更新,更高階的語言元素.
我們第一次接觸它是在我們要給Takipi新增Scala支援的時候, 我們不得不深入研究Scala的編譯器.伴隨著JAVA8的來臨,我認為探究Scala和java編譯器是如何實現Lambda表示式是非常有趣的事情.結果也是相當出人意料.
接下來,我展示一個簡單的Lambda表示式,用於將字串集合轉化成字串自身長度的集合。
Java的寫法 –
List 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的實現方式
The Code
我使用javap(jdk自帶的工具)去檢視Scala編譯器編譯出來的class類中所包含的位元組碼內容。讓我們一起看看最終的位元組碼(這是JVM將真正執行的)
// 載入names物件引用,壓入操作棧(JVM把它當成變數#2) // 它將停留一會,直到被map函式呼叫. 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這樣”端莊”的淑女也可以教我們一些新的花樣真是非常讓人高興。