λ表達式之爭:Scala vs Java8

jopen 9年前發布 | 23K 次閱讀 Java8 Java開發

本文由 ImportNew - paddx 翻譯自 javacodegeeks。 


最近幾年Lambda表達式風靡于編程界。很多現代編程語言都把它作為函數式編程的基本組成部分。基于JVM的編程語言如Scala、Groovy及Clojure把它作為關鍵部分集成在語言中。而如今,(最終)Java 8也加入了這個有趣的行列。

Lambda表達式最有意思的地方在于,在JVM的角度來看它是完全不可見的。在JVM中沒有匿名函數或Lambda表達式的概念。JVM唯一知道是字節碼。字節碼是一個嚴格的OO規范。由語言的創造者和編譯者通過這些限制來創建新的、高級的語言元素。

我們第一次遇到Lambda表達式是需要在Takipi中增加對Scala的支持,所以不得不深入了解Scala的編譯器。而這時Java 8也正處在關鍵時刻。我猜想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開始

λ表達式之爭:Scala vs Java8

代碼

我使用 javap 來查看通過Scala編譯器生成的.class文件的字節碼的內容。讓我們看一下字節碼的結果(這才是JVM真正執行的內容)。

//將變量名加載到棧中(JVM視為變量#2),先保存在這,之后會在map函數中用到
aload_2

接下來的事情就變得更有趣了,一個由編譯器生成的synthetic的實例創建并初始化(譯者注:Synthetic類是指由JVM運行時生成的類)。非常有意思的是,Lambda作為整個方法的一部分來定義的,但它實際上完全存在于我們類的外部。

new myLambdas/Lambda1$$anonfun$1 //實例化Lambda對象
dup //把它加入棧中
//最后,調用構造函數.記住,這是源自JVM的一個簡單對象
invokespecial myLambdas/Lambda1$$anonfun$1/()V
//這個兩行加載immutable.List CanBuildFrom工廠,該工廠能生成新的list。工廠模式是Scala的集合架構的一部分。
getstatic scala/collection/immutable/List$/MODULE$
Lscala/collection/immutable/List$;
invokevirtual scala/collection/immutable/List$/canBuildFrom()
Lscala/collection/generic/CanBuildFrom;

//現在,棧上已經有了Lambda對象及工廠,下一階段就可以調用map函數。 //你應該還記得,我們在一開始的時候將名稱變量加載到了棧中。我們現在可以用它來實現map方法的調用了。 //map方法接受一個Lambda對象和一個工廠,生成一個長度的list。

invokevirtual scala/collection/immutable/List/map(Lscala/Function1; Lscala/collection/generic/CanBuildFrom;)Ljava/lang/Object;</pre>

但是請稍等,Lambda對象內部做了什么事情?

Lambda對象

Lambda類來繼承自scala.runtime.AbstractFunction1。通過這種方式,map() 函數可以多態調用重寫后的 apply() 方法,apply()代碼如下:

//這段代碼是加載this及目標對象,檢測它是不是一個字符串,然后調用另一個重載后的、真正工作的apply方法,最后包裝返回結果
aload_0//加載this
aload_1//加載字符串參數
checkcast java/lang/String//確保是一個字符串 - 得到一個Object

// 調用synthetic類的apply()方法 invokevirtual myLambdas/Lambda1$$anonfun$1/apply(Ljava/lang/String;)I

//包裝結果 invokestatic scala/runtime/BoxesRunTime/boxToInteger(I)Ljava/lang/Integer areturn</pre>

真正的執行.length() 操作的代碼嵌套在另個一apply方法中,該方法正如我們期望的一樣,簡單的返回了字符串的長度。

唷……,走了好長的一段路才到這。

aload_1
invokevirtual java/lang/String/length()I
ireturn

我們在上面只是寫了一行簡單的代碼,但是卻產生了許多的字節碼,包括一個額外的類和一堆方法。但是,這絕不是在勸阻我們不要用Lambda(我們是在Scala中寫代碼,而不是C)。這僅僅是為了展示這種結構后面的復雜性。

我相當期待Java 8也是用這種方式實現的,但是令人驚訝的時,java采取了完全不同的方式。

Java 8:一種新的方式

Java 8產生的字節碼比較短,但是還有更令人驚訝的東西。它剛開始簡單的加載了名稱變量,然后調用 stream() 方法,但是接下做了一些非常好的優化。它沒有創建一個新的對象來包裝Lambda函數,而是使用了新的 invokeDynamic 指令,該指令是Java 7時增加的,這個地方的用于調用真實的Lambda函數。

aload_1 // 加載名稱變量
//調用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魔法:這條JVM指令在Java 7中增加,用于減少JVM的限制,允許動態語言在運行時綁定符號。而在這之前,所有的鏈接都是靜態的,在代碼編譯的時候就由JVM完成。

動態鏈接:如果你看過invokedynamic指令,你會發現沒有引用指向真正的Lambda函數(即lambda$0)。答案歸結于invokedynamic指令的設計,但是更簡短的答案是Lambda表達式的簽名,就我們的例子來說是

//一個名為lamda$0的函數,獲取一個字符串,返回一個整數
lambdas/Lambda1.lambda$0:(Ljava/lang/String;)Ljava/lang/Integer;

存儲在.class的一個單獨的表中,該表作為#0參數傳遞給指令。這個新的表確實改變了字節碼規范的結構,這是多年之后的第一次改變,這同樣需要采取Takipi的錯誤分析引擎。

Lambda代碼

這段代碼是真正的Lambda表達式。非常容易,簡單地加載字符串參數,調用length()方法并包裝成結果。請注意,它是編譯成了一個靜態函數,避免像之前看到的Scala一樣,傳入額外的this對象。

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這樣“端莊”的女士能教我們一些戲法。

原文鏈接: javacodegeeks 翻譯: ImportNew.com - paddx
譯文鏈接: http://www.importnew.com/16020.html

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!