
最近幾年Lambda運算式風靡于編程界. 很多現代編程語言都把它作為函式式編程的基本組成部分.
基于JVM的編程語言如Scala,Groovy還有Clojure把它們作為關鍵部分集成在語言中.現在Java8也加入了它們的行列.
有趣的是,對于JVM來說,Lambda運算式是完全不可見的,并沒有匿名函式和Lamada運算式的概念,它只知道位元組碼是嚴格面向物件規范的.它取決于語言的作者和它的編譯器在規范限制內創造出更新,更高級的語言元素.
我們第一次接觸它是在我們要給Takipi添加Scala支持的時候, 我們不得不深入研究Scala的編譯器.伴隨著JAVA8的來臨,我認為探究Scala和java編譯器是如何實作Lambda運算式是非常有趣的事情.結果也是相當出人意料.
接下來,我展示一個簡單的Lambda運算式,用于將字串集合轉化成字串自身長度的集合,
Java的寫法 –
1List names = Arrays.asList("1", "2", "3");
2Stream lengths = names.stream().map(name -> name.length());
Scala的寫法 –
1.val names = List("1", "2", "3")
2.val lengths = names.map(name =>name.length)
表面上看起來非常簡單,那么后面的復雜東西是怎么搞的呢?
一起分析Scala的實作方式

The Code
我使用javap(jdk自帶的工具)去查看Scala編譯器編譯出來的class類中所包含的位元組碼內容,讓我們一起看看最終的位元組碼(這是JVM將真正執行的)
1.// 加載names物件參考,壓入操作堆疊(JVM把它當成變數#2)
2.// 它將停留一會,直到被map函式呼叫.
3.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這樣”端莊”的淑女也可以教我們一些新的花樣真是非常讓人高興,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/180694.html
標籤:Java
