本文收錄于 kotlin入門潛修專題系列,歡迎學(xué)習(xí)交流襟己。
創(chuàng)作不易,如有轉(zhuǎn)載牍陌,還請備注擎浴。
高階方法及l(fā)ambda表達式原理
照例,先看下我們要分析的源代碼片段毒涧。如下所示:
class Test {
//定義了一個高階方法m0
fun m0(checkStr: () -> String) {
checkStr()
}
//測試方法贮预,用于測試m0
fun test(){
m0 { -> "return a str" }//采用lambda表達式實例化方法類型
}
}
一個最簡單的高階方法及l(fā)ambda表達式的示例,來看下對應(yīng)的字節(jié)碼契讲,照例先全部粘貼下:
public final class Test {
// access flags 0x11
// signature (Lkotlin/jvm/functions/Function0<Ljava/lang/String;>;)V
// declaration: void m0(kotlin.jvm.functions.Function0<java.lang.String>)
public final m0(Lkotlin/jvm/functions/Function0;)V
@Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
L0
ALOAD 1
LDC "checkStr"
INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
L1
LINENUMBER 3 L1
ALOAD 1
INVOKEINTERFACE kotlin/jvm/functions/Function0.invoke ()Ljava/lang/Object;
POP
L2
LINENUMBER 4 L2
RETURN
L3
LOCALVARIABLE this LTest; L0 L3 0
LOCALVARIABLE checkStr Lkotlin/jvm/functions/Function0; L0 L3 1
MAXSTACK = 2
MAXLOCALS = 2
// access flags 0x11
public final test()V
L0
LINENUMBER 7 L0
ALOAD 0
GETSTATIC Test$test$1.INSTANCE : LTest$test$1;
CHECKCAST kotlin/jvm/functions/Function0
INVOKEVIRTUAL Test.m0 (Lkotlin/jvm/functions/Function0;)V
L1
LINENUMBER 8 L1
RETURN
L2
LOCALVARIABLE this LTest; L0 L2 0
MAXSTACK = 2
MAXLOCALS = 1
// access flags 0x1
public <init>()V
L0
LINENUMBER 1 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this LTest; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x18
final static INNERCLASS Test$test$1 null null
// compiled from: Main.kt
}
// ================Test$test$1.class =================
// class version 50.0 (50)
// access flags 0x30
// signature Lkotlin/jvm/internal/Lambda;Lkotlin/jvm/functions/Function0<Ljava/lang/String;>;
// declaration: Test$test$1 extends kotlin.jvm.internal.Lambda implements kotlin.jvm.functions.Function0<java.lang.String>
final class Test$test$1 extends kotlin/jvm/internal/Lambda implements kotlin/jvm/functions/Function0 {
// access flags 0x1041
public synthetic bridge invoke()Ljava/lang/Object;
L0
LINENUMBER 1 L0
ALOAD 0
INVOKEVIRTUAL Test$test$1.invoke ()Ljava/lang/String;
ARETURN
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x11
public final invoke()Ljava/lang/String;
@Lorg/jetbrains/annotations/NotNull;() // invisible
L0
LINENUMBER 7 L0
LDC "return a str"
L1
ARETURN
L2
LOCALVARIABLE this LTest$test$1; L0 L2 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x0
<init>()V
ALOAD 0
ICONST_0
INVOKESPECIAL kotlin/jvm/internal/Lambda.<init> (I)V
RETURN
MAXSTACK = 2
MAXLOCALS = 1
// access flags 0x19
public final static LTest$test$1; INSTANCE
// access flags 0x8
static <clinit>()V
NEW Test$test$1
DUP
INVOKESPECIAL Test$test$1.<init> ()V
PUTSTATIC Test$test$1.INSTANCE : LTest$test$1;
RETURN
MAXSTACK = 2
MAXLOCALS = 0
OUTERCLASS Test test ()V
// access flags 0x18
final static INNERCLASS Test$test$1 null null
// compiled from: Main.kt
}
字節(jié)碼比較長仿吞,這里照例分割成幾個部分來進行闡述。
- 高階方法中的方法類型實際上會被編譯成Function類型捡偏,在本例中的類型就是Function0唤冈,其對應(yīng)的字節(jié)碼如下所示:
public final m0(Lkotlin/jvm/functions/Function0;)V
這就是kotlin中對方法類型的處理,那么是所有的方法類型都會被編譯成Function0嗎银伟?當(dāng)然不是你虹,kotlin會根據(jù)方法類型的參數(shù)個數(shù)來做相應(yīng)的處理,比如現(xiàn)在方法類型有1個入?yún)⑼埽瑒t就會被編譯成Function1類型傅物,有2個入?yún)⒕蜁痪幾g成Function2類型,一次類推琉预。下面我粘貼一個有兩個入?yún)⒌淖止?jié)碼編譯案例:
//源代碼m0董饰,其入?yún)⑹怯袃蓚€參數(shù)的方法類型
fun m0(checkStr: (str1:String, str2:String) -> String) {
}
//編譯后對應(yīng)的字節(jié)碼,從中可知模孩,
//兩個參數(shù)的方法類型被編譯成了Function2類型
public final m0(Lkotlin/jvm/functions/Function2;)V
事實上尖阔,我們不從字節(jié)碼的角度也能發(fā)現(xiàn)這個規(guī)律,kotlin所有的方法類型都會被處理成Function家族類型榨咐,只不過參數(shù)個數(shù)不同介却,對應(yīng)的具體的Function類型不同而已,這個可以通過kotlin.jvm.functions包下的接口定義得到確認块茁,如下所示:
package kotlin.jvm.functions
/** A function that takes 0 arguments. */
public interface Function0<out R> : Function<R> {
/** Invokes the function. */
public operator fun invoke(): R
}
/** A function that takes 1 argument. */
public interface Function1<in P1, out R> : Function<R> {
/** Invokes the function with the specified argument. */
public operator fun invoke(p1: P1): R
}
/** A function that takes 2 arguments. */
public interface Function2<in P1, in P2, out R> : Function<R> {
/** Invokes the function with the specified arguments. */
public operator fun invoke(p1: P1, p2: P2): R
}
//中間省略n個Function類型
.........................................................
.........................................................
//第21個參數(shù)對應(yīng)的Function類型
/** A function that takes 21 arguments. */
public interface Function21<in P1, in P2, in P3, in P4, in P5, in P6, in P7, in P8, in P9, in P10, in P11, in P12, in P13, in P14, in P15, in P16, in P17, in P18, in P19, in P20, in P21, out R> : Function<R> {
/** Invokes the function with the specified arguments. */
public operator fun invoke(p1: P1, p2: P2, p3: P3, p4: P4, p5: P5, p6: P6, p7: P7, p8: P8, p9: P9, p10: P10, p11: P11, p12: P12, p13: P13, p14: P14, p15: P15, p16: P16, p17: P17, p18: P18, p19: P19, p20: P20, p21: P21): R
}
/** A function that takes 22 arguments. */
public interface Function22<in P1, in P2, in P3, in P4, in P5, in P6, in P7, in P8, in P9, in P10, in P11, in P12, in P13, in P14, in P15, in P16, in P17, in P18, in P19, in P20, in P21, in P22, out R> : Function<R> {
/** Invokes the function with the specified arguments. */
public operator fun invoke(p1: P1, p2: P2, p3: P3, p4: P4, p5: P5, p6: P6, p7: P7, p8: P8, p9: P9, p10: P10, p11: P11, p12: P12, p13: P13, p14: P14, p15: P15, p16: P16, p17: P17, p18: P18, p19: P19, p20: P20, p21: P21, p22: P22): R
}
由上面的代碼可知齿坷,kotlin最多支持定義有22個入?yún)⒌姆椒愋凸鸺 _@些方法都有一個共同的Function<R>,F(xiàn)unction<R>代表的正是方法類型永淌。除此之外崎场,每個方法類型都提供了一個包含有對應(yīng)參數(shù)個數(shù)的invoke操作符。
- 在m0方法中調(diào)用傳入的方法類型實例的時候遂蛀,實際上就是通過方法的invoke來完成調(diào)用的谭跨,對應(yīng)的字節(jié)碼摘錄如下:
L1
LINENUMBER 3 L1
ALOAD 1
INVOKEINTERFACE
kotlin/jvm/functions/Function0.invoke ()Ljava/lang/Object;
POP
L2
有上述代碼可知,kotlin最終是通過Function0.invoke ()來完成了checkStr方法的調(diào)用李滴。
- kotlin會為lambda表達式生成一個新類螃宙,類名為自己所處的類名(Test)+ 所處的方法名(test)+ 數(shù)字(從1開始,有多個則依次遞增)所坯。該類繼承了Lambda類并實現(xiàn)了對應(yīng)的Function接口谆扎。字節(jié)碼如下所示:
//生成的新類繼承了Lambda類,并實現(xiàn)了Function0接口
final class Test$test$2 extends kotlin/jvm/internal/Lambda implements kotlin/jvm/functions/Function0 {
Lambda類實際上沒有什么東西芹助,就是提供了lambda參數(shù)數(shù)量的入口:getArity堂湖。而Function0正是我們上面提到的有invoke入口的接口。
- 當(dāng)調(diào)用方法m0的時候状土,傳入的lambda實例无蜂,實際上就是通過上述生成的新類實例完成的。而且實際調(diào)用的時候也正是通過上述生成的新類的invoke方法完成調(diào)用的蒙谓,對應(yīng)的字節(jié)碼如下所示:
//test方法對應(yīng)的字節(jié)碼
// access flags 0x11
public final test()V
L0
LINENUMBER 7 L0
ALOAD 0
//這里實際上是調(diào)用了生成類LTest$test$1的實例
GETSTATIC Test$test$1.INSTANCE : LTest$test$1;
CHECKCAST kotlin/jvm/functions/Function0
INVOKEVIRTUAL Test.m0 (Lkotlin/jvm/functions/Function0;)V//將LTest$test$1實例傳入方法m0
L1
- 步驟4中提到的INSTANCE酱讶,實際上是kotlin為lambda表達式生成的新類、提供了一個單例對象彼乌。該單例對象是在該類的類構(gòu)造方法中完成初始化的,如下所示:
//本例中Test$test$1的成員屬性INSTANCE
public final static LTest$test$1; INSTANCE
//Test$test$1的類構(gòu)造方法
static <clinit>()V
NEW Test$test$1
DUP
//這里調(diào)用了類的實例構(gòu)造方法渊迁,即生成了一個Test$test$1對象
INVOKESPECIAL Test$test$1.<init> ()V
//這里完成了對INSTANCE的賦值慰照,其值正式上面生成的新對象
PUTSTATIC Test$test$1.INSTANCE : LTest$test$1;
RETURN
MAXSTACK = 2
MAXLOCALS = 0
- 在kotlin為我們生成的Test$test$1類中的invoke方法中,正式完成了方法實例對應(yīng)的實現(xiàn)琉朽,如下所示:
//這里就是invoke的實現(xiàn)毒租,lambda表達式實例對應(yīng)的實現(xiàn)
//就是在這里。
public final invoke()Ljava/lang/String;
@Lorg/jetbrains/annotations/NotNull;() // invisible
L0
LINENUMBER 7 L0
LDC "return a str"
L1
ARETURN
L2
LOCALVARIABLE this LTest$test$1; L0 L2 0
MAXSTACK = 1
MAXLOCALS = 1
至此箱叁,高階方法與lambda背后的原理產(chǎn)生完畢墅垮。下面看下匿名方法的實現(xiàn)原理。
匿名方法背后的原理
我們將上述代碼的調(diào)用改成匿名方法的實現(xiàn)耕漱,如下所示:
//將m0方法的調(diào)用改成傳入匿名方法實例方式
fun test() {
m0(fun(): String { return "return a str" })
}
通過查看對應(yīng)的字節(jié)碼發(fā)現(xiàn)算色,匿名方法和lambda表達式對應(yīng)的字節(jié)碼完全一樣,重要的字節(jié)碼摘錄如下所示:
//同樣生成了一個新類螟够,同lambda表達式一致
final class Test$test$1 extends kotlin/jvm/internal/Lambda implements kotlin/jvm/functions/Function0 {
// 也提供了一個單例實現(xiàn)
public final static LTest$test$1; INSTANCE
//匿名方法的實現(xiàn)也在invoke完成
public final invoke()Ljava/lang/String;
@Lorg/jetbrains/annotations/NotNull;() // invisible
L0
LINENUMBER 7 L0
LDC "return a str"
ARETURN
L1
LOCALVARIABLE this LTest$test$1; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
//等等...
上面省略了一些分析灾梦,最終總結(jié)一句話峡钓,匿名方法和lambda表達式實現(xiàn)一致。
閉包的訪問原理
在上篇高階方法及l(fā)ambda表達式中若河,我們提到了閉包的概念能岩,那么為什么lambda表達式和匿名方法能夠自由訪問外部作用域中的成員呢?本小節(jié)來分析下原因萧福。
首先附上我們要分析的源代碼:
class Test {
//為了更能說服問題拉鹃,這里提供了一個private的私有屬性
private val str:String = "test"
fun m0(checkStr: () -> String) {
}
//測試方法,這里完成了對外部作用域的訪問鲫忍,即$str
fun test() {
m0(fun(): String { return "return a $str" })
}
}
上述代碼生成的字節(jié)碼這里不再全部粘貼膏燕,直接撿重點的來看下:
- kotlin編譯器會為私有是成員str生成一個公有的訪問方法,對應(yīng)字節(jié)碼如下所示:
//位于Test類中饲窿,由編譯器為我們自動生成的訪問str的共有方法
public final static synthetic access$getStr$p(LTest;)Ljava/lang/String;
@Lorg/jetbrains/annotations/NotNull;() // invisible
L0
LINENUMBER 1 L0
ALOAD 0
GETFIELD Test.str : Ljava/lang/String;
ARETURN
L1
LOCALVARIABLE $this LTest; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
- 在生成的新類中煌寇,會持有Test類的引用,并在實例構(gòu)造方法中將傳入的Test類對象賦值給了Test$test$1.this$0逾雄,對應(yīng)字節(jié)碼如下所示:
//位于生成的Test$test$1類中
<init>(LTest;)V
ALOAD 0
ALOAD 1
//這里實際上將傳入的Test對象賦值給了Test$test$1.this$0
PUTFIELD Test$test$1.this$0 : LTest;
ALOAD 0
ICONST_0
INVOKESPECIAL kotlin/jvm/internal/Lambda.<init> (I)V
RETURN
MAXSTACK = 2
MAXLOCALS = 2
- 步驟2中的Test對象阀溶,實際上是在test方法調(diào)用的時候生成的,對應(yīng)的字節(jié)碼如下所示:
//test方法對應(yīng)的字節(jié)碼
public final test()V
L0
LINENUMBER 7 L0
ALOAD 0
NEW Test$test$1//生成一個新的Test$test$1對象
DUP
ALOAD 0
//這句字節(jié)碼表示傳入了Test對象this
INVOKESPECIAL Test$test$1.<init> (LTest;)V
CHECKCAST kotlin/jvm/functions/Function0
INVOKEVIRTUAL Test.m0 (Lkotlin/jvm/functions/Function0;)V
- kotlin正式通過上述三個步驟完成了對閉包環(huán)境下的訪問鸦泳,因為方法體實現(xiàn)都在新生成類中的invoke中银锻,所以其對應(yīng)的字節(jié)碼如下所示:
public final invoke()Ljava/lang/String;
@Lorg/jetbrains/annotations/NotNull;() // invisible
L0
LINENUMBER 7 L0
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "return a "
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 0
GETFIELD Test$test$1.this$0 : LTest;
//這里完成了對str的訪問
INVOKESTATIC Test.access$getStr$p (LTest;)Ljava/lang/String;
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
ARETURN
上面代碼最主要有兩句,這里再次摘錄如下:
//獲取Test$test$1.this$0實例
GETFIELD Test$test$1.this$0 : LTest;
//這里通過生成的共有方法access$getStr$p 完成了對str的訪問
INVOKESTATIC Test.access$getStr$p (LTest;)Ljava/lang/String;
至此做鹰,高階方法相關(guān)的原理分析完畢击纬。