摘要
本文通過 Scala 語言來實(shí)現(xiàn)一個簡單的閉包紊遵,并且通過 Opcode 來深入理解 Scala 中閉包的實(shí)現(xiàn)原理。
一個簡單的例子
閉包,簡單的理解就是:函數(shù)內(nèi)部的變量不在其作用于時,仍然可以從外部進(jìn)行訪問痢艺,聽上去有些抽象仓洼;
下面我們來通過一個簡單的例子實(shí)現(xiàn) Scala 中的閉包,代碼如下:
object Closures {
def main(args: Array[String]): Unit = {
val addOne = makeAdd(1)
val addTwo = makeAdd(2)
println(addOne(1))
println(addTwo(1))
}
def makeAdd(more: Int) = (x: Int) => x + more
def normalAdd(a: Int, b: Int) = a + b
}
我們定義了一個函數(shù) makeAdd堤舒,輸入?yún)?shù)是 Int 類型色建,返回的是一個函數(shù)(其實(shí)可以看成函數(shù),后面我們會深入去研究到底是什么)舌缤,同樣我們定義了一個普通的函數(shù) normalAdd 來進(jìn)行比較箕戳,main 方法中,首先我們通過調(diào)用 makeAdd 來定義了兩個 val:addOne 和 addTwo 并分別傳入 1 和 2友驮,然后執(zhí)行并打印 addOne(1) 和 addTwo(2)漂羊,運(yùn)行的結(jié)果是 2 和 3。
分析
接下來我們來詳細(xì)的分析一下上面這個例子的 Opcode卸留,通過 javap 命令來查看 Closures.class 的字節(jié)碼:
Last modified Feb 14, 2017; size 1311 bytes
MD5 checksum 02722b1fa1195c63a6dd0c8615db8c26
Compiled from "Closures.scala"
public final class com.learn.scala.Closures$
minor version: 0
major version: 50
flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER
Constant pool:
#1 = Utf8 com/learn/scala/Closures$
#2 = Class #1 // com/learn/scala/Closures$
#3 = Utf8 java/lang/Object
#4 = Class #3 // java/lang/Object
#5 = Utf8 Closures.scala
#6 = Utf8 MODULE$
#7 = Utf8 Lcom/learn/scala/Closures$;
#8 = Utf8 <clinit>
#9 = Utf8 ()V
#10 = Utf8 <init>
#11 = NameAndType #10:#9 // "<init>":()V
#12 = Methodref #2.#11 // com/learn/scala/Closures$."<init>":()V
#13 = Utf8 main
#14 = Utf8 ([Ljava/lang/String;)V
#15 = Utf8 makeAdd
#16 = Utf8 (I)Lscala/Function1;
#17 = NameAndType #15:#16 // makeAdd:(I)Lscala/Function1;
#18 = Methodref #2.#17 // com/learn/scala/Closures$.makeAdd:(I)Lscala/Function1;
#19 = Utf8 scala/Predef$
#20 = Class #19 // scala/Predef$
#21 = Utf8 Lscala/Predef$;
#22 = NameAndType #6:#21 // MODULE$:Lscala/Predef$;
#23 = Fieldref #20.#22 // scala/Predef$.MODULE$:Lscala/Predef$;
#24 = Utf8 scala/Function1
#25 = Class #24 // scala/Function1
#26 = Utf8 apply$mcII$sp
#27 = Utf8 (I)I
#28 = NameAndType #26:#27 // apply$mcII$sp:(I)I
#29 = InterfaceMethodref #25.#28 // scala/Function1.apply$mcII$sp:(I)I
#30 = Utf8 scala/runtime/BoxesRunTime
#31 = Class #30 // scala/runtime/BoxesRunTime
#32 = Utf8 boxToInteger
#33 = Utf8 (I)Ljava/lang/Integer;
#34 = NameAndType #32:#33 // boxToInteger:(I)Ljava/lang/Integer;
#35 = Methodref #31.#34 // scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer;
#36 = Utf8 println
#37 = Utf8 (Ljava/lang/Object;)V
#38 = NameAndType #36:#37 // println:(Ljava/lang/Object;)V
#39 = Methodref #20.#38 // scala/Predef$.println:(Ljava/lang/Object;)V
#40 = Utf8 this
#41 = Utf8 args
#42 = Utf8 [Ljava/lang/String;
#43 = Utf8 addOne
#44 = Utf8 Lscala/Function1;
#45 = Utf8 addTwo
#46 = Utf8 com/learn/scala/Closures$$anonfun$makeAdd$1
#47 = Class #46 // com/learn/scala/Closures$$anonfun$makeAdd$1
#48 = Utf8 (I)V
#49 = NameAndType #10:#48 // "<init>":(I)V
#50 = Methodref #47.#49 // com/learn/scala/Closures$$anonfun$makeAdd$1."<init>":(I)V
#51 = Utf8 more
#52 = Utf8 I
#53 = Utf8 normalAdd
#54 = Utf8 (II)I
#55 = Utf8 a
#56 = Utf8 b
#57 = Methodref #4.#11 // java/lang/Object."<init>":()V
#58 = NameAndType #6:#7 // MODULE$:Lcom/learn/scala/Closures$;
#59 = Fieldref #2.#58 // com/learn/scala/Closures$.MODULE$:Lcom/learn/scala/Closures$;
#60 = Utf8 Code
#61 = Utf8 LocalVariableTable
#62 = Utf8 LineNumberTable
#63 = Utf8 Signature
#64 = Utf8 (I)Lscala/Function1<Ljava/lang/Object;Ljava/lang/Object;>;
#65 = Utf8 SourceFile
#66 = Utf8 InnerClasses
#67 = Utf8 ScalaInlineInfo
#68 = Utf8 Scala
{
public static final com.learn.scala.Closures$ MODULE$;
descriptor: Lcom/learn/scala/Closures$;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
public static {};
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: new #2 // class com/learn/scala/Closures$
3: invokespecial #12 // Method "<init>":()V
6: return
public void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=3, locals=4, args_size=2
0: aload_0
1: iconst_1
2: invokevirtual #18 // Method makeAdd:(I)Lscala/Function1;
5: astore_2
6: aload_0
7: iconst_2
8: invokevirtual #18 // Method makeAdd:(I)Lscala/Function1;
11: astore_3
12: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$;
15: aload_2
16: iconst_1
17: invokeinterface #29, 2 // InterfaceMethod scala/Function1.apply$mcII$sp:(I)I
22: invokestatic #35 // Method scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer;
25: invokevirtual #39 // Method scala/Predef$.println:(Ljava/lang/Object;)V
28: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$;
31: aload_3
32: iconst_1
33: invokeinterface #29, 2 // InterfaceMethod scala/Function1.apply$mcII$sp:(I)I
38: invokestatic #35 // Method scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer;
41: invokevirtual #39 // Method scala/Predef$.println:(Ljava/lang/Object;)V
44: return
LocalVariableTable:
Start Length Slot Name Signature
0 45 0 this Lcom/learn/scala/Closures$;
0 45 1 args [Ljava/lang/String;
6 38 2 addOne Lscala/Function1;
12 32 3 addTwo Lscala/Function1;
LineNumberTable:
line 6: 0
line 7: 6
line 9: 12
line 10: 28
public scala.Function1<java.lang.Object, java.lang.Object> makeAdd(int);
descriptor: (I)Lscala/Function1;
flags: ACC_PUBLIC
Code:
stack=3, locals=2, args_size=2
0: new #47 // class com/learn/scala/Closures$$anonfun$makeAdd$1
3: dup
4: iload_1
5: invokespecial #50 // Method com/learn/scala/Closures$$anonfun$makeAdd$1."<init>":(I)V
8: areturn
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/learn/scala/Closures$;
0 9 1 more I
LineNumberTable:
line 13: 0
Signature: #64 // (I)Lscala/Function1<Ljava/lang/Object;Ljava/lang/Object;>;
public int normalAdd(int, int);
descriptor: (II)I
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: iload_1
1: iload_2
2: iadd
3: ireturn
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 this Lcom/learn/scala/Closures$;
0 4 1 a I
0 4 2 b I
LineNumberTable:
line 15: 0
}
SourceFile: "Closures.scala"
InnerClasses:
public final #47; //class com/learn/scala/Closures$$anonfun$makeAdd$1
Error: unknown attribute
ScalaInlineInfo: length = 0x18
01 01 00 04 00 0A 00 09 01 00 0D 00 0E 01 00 0F
00 10 01 00 35 00 36 01
Error: unknown attribute
Scala: length = 0x0
我們先來看一下 makeAdd 部分(為了方便理解,下文中的圖中只是簡單的表示椭豫,類似常量池的部分并沒有在圖中表現(xiàn)出來)
首先通過 new 實(shí)例化了一個 class com/learn/scala/Closures$$anonfun$makeAdd$1耻瑟,圖中的 Heap 中簡單的表示為 makeAdd;
dup 命令就是復(fù)制一份上一步驟分配的空間的引用赏酥,并壓入 operand stack 的棧底
iload_1 就是將 LocalVariableTable 中的 more 壓入 operand stack
然后 invokespecial 將棧中的兩個值 pop 出來執(zhí)行 init 操作喳整,也就是將 more 的具體值傳入到 makeAdd 的初始化操作的函數(shù)中(可以認(rèn)為是構(gòu)造函數(shù)),然后將得到的新的實(shí)例化對象的引用壓入 operand stack
最后使用 areturn 返回這個引用裸扶,即將 initedmakeAddRef pop 出來
由此可以看出框都,Scala 中實(shí)際上是在 Heap 中創(chuàng)建了一個 makeAdd 的實(shí)例化對象,所以 more 變量在下次調(diào)用的時候依然可以使用呵晨,而普通方法的局部變量在調(diào)用的時候是壓入 Operand stack 中的魏保,計(jì)算完成之后就會 pop 出,所以在函數(shù)的調(diào)用完成后就不能在訪問這個變量摸屠,下面我們來通過 main 方法中的具體執(zhí)行來驗(yàn)證此結(jié)論谓罗。
我們只看關(guān)鍵的部分:
iconst_1:將 1 壓入 operand stack
invokevirtual #18 將執(zhí)行上面我們分析過的 makeAdd 函數(shù)調(diào)用的部分,調(diào)用的時候?qū)?1 作為參數(shù)傳入進(jìn)行初始化季二,并將最終得到的實(shí)例對象的引用壓入 operand stack
astore_2 將上一步驟的引用 pop 出來并存儲到 Local Variable Array 中
至此執(zhí)行了 main 方法中的 val addOne = makeAdd(1)
7檩咱、8、11 重復(fù)執(zhí)行了上面的步驟胯舷,即代碼中的第二行:val addTwo = makeAdd(2)
此時刻蚯,heap 有兩個對象的實(shí)例,然后后面的 15桑嘶、16炊汹、17、22不翩、25 和 31兵扬、32麻裳、33、38器钟、41 分別執(zhí)行了println(addOne(1))
和println(addTwo(1))
的部分津坑,比較關(guān)鍵的部分是 invokeinterface 的時候執(zhí)行了具體對象的 apply 方法,并將 1 作為參數(shù)傳入傲霸,其它部分在此不再詳細(xì)說明疆瑰。
實(shí)際上我們可以在 bin 目錄下看到一個名為Closures$$anonfun$makeAdd$1的類文件,我們用 javap 命令可以看到昙啄,實(shí)際上是調(diào)用了實(shí)例對象的 apply 方法:
public final int apply(int);
descriptor: (I)I
flags: ACC_PUBLIC, ACC_FINAL
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: iload_1
2: invokevirtual #23 // Method apply$mcII$sp:(I)I
5: ireturn
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this Lcom/learn/scala/Closures$$anonfun$makeAdd$1;
0 6 1 x I
LineNumberTable:
line 13: 0
至此穆役,我們驗(yàn)證了我們的結(jié)論,Scala 實(shí)現(xiàn)閉包的方法是在 heap 中保存了使用不同參數(shù)初始化而產(chǎn)生的不同對象梳凛,對象中保存了變量的狀態(tài)耿币,然后調(diào)用具體對象的 apply 方法而最后產(chǎn)生不同的結(jié)果。
結(jié)論
與 Java 中使用內(nèi)部類實(shí)現(xiàn)閉包相比韧拒,Scala 中為函數(shù)創(chuàng)建了一個對象 Function1 來保存變量的狀態(tài)淹接,然后具體執(zhí)行的時候調(diào)用對應(yīng)實(shí)例的 apply 方法,實(shí)現(xiàn)了函數(shù)作用域外也可以訪問函數(shù)內(nèi)部的變量叛溢。
希望本文對大家有所幫助塑悼,并歡迎大家批評指正文中有可能出現(xiàn)的紕漏。