1. 程式人生 > >編譯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的寫法 –

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的實現方式

SCalaLam-1

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  – 新的實現方式

JavaLam-1

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這樣”端莊”的淑女也可以教我們一些新的花樣真是非常讓人高興。

fredForBlog


樑海艦

阿里雲工程師,Java研發、關注多執行緒、併發程式設計。