本文收錄于 kotlin入門潛修專題系列,歡迎學(xué)習(xí)交流。
創(chuàng)作不易,如有轉(zhuǎn)載,還請(qǐng)備注。
高階方法及l(fā)ambda表達(dá)式原理
照例,先看下我們要分析的源代碼片段。如下所示:
class Test {
//定義了一個(gè)高階方法m0
fun m0(checkStr: () -> String) {
checkStr()
}
//測(cè)試方法,用于測(cè)試m0
fun test(){
m0 { -> "return a str" }//采用lambda表達(dá)式實(shí)例化方法類型
}
}
一個(gè)最簡(jiǎn)單的高階方法及l(fā)ambda表達(dá)式的示例,來(lái)看下對(duì)應(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é)碼比較長(zhǎng),這里照例分割成幾個(gè)部分來(lái)進(jìn)行闡述。
- 高階方法中的方法類型實(shí)際上會(huì)被編譯成Function類型,在本例中的類型就是Function0,其對(duì)應(yīng)的字節(jié)碼如下所示:
public final m0(Lkotlin/jvm/functions/Function0;)V
這就是kotlin中對(duì)方法類型的處理,那么是所有的方法類型都會(huì)被編譯成Function0嗎?當(dāng)然不是,kotlin會(huì)根據(jù)方法類型的參數(shù)個(gè)數(shù)來(lái)做相應(yīng)的處理,比如現(xiàn)在方法類型有1個(gè)入?yún)?,則就會(huì)被編譯成Function1類型,有2個(gè)入?yún)⒕蜁?huì)被編譯成Function2類型,一次類推。下面我粘貼一個(gè)有兩個(gè)入?yún)⒌淖止?jié)碼編譯案例:
//源代碼m0,其入?yún)⑹怯袃蓚€(gè)參數(shù)的方法類型
fun m0(checkStr: (str1:String, str2:String) -> String) {
}
//編譯后對(duì)應(yīng)的字節(jié)碼,從中可知,
//兩個(gè)參數(shù)的方法類型被編譯成了Function2類型
public final m0(Lkotlin/jvm/functions/Function2;)V
事實(shí)上,我們不從字節(jié)碼的角度也能發(fā)現(xiàn)這個(gè)規(guī)律,kotlin所有的方法類型都會(huì)被處理成Function家族類型,只不過(guò)參數(shù)個(gè)數(shù)不同,對(duì)應(yīng)的具體的Function類型不同而已,這個(gè)可以通過(guò)kotlin.jvm.functions包下的接口定義得到確認(rèn),如下所示:
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個(gè)Function類型
.........................................................
.........................................................
//第21個(gè)參數(shù)對(duì)應(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個(gè)入?yún)⒌姆椒愋?。這些方法都有一個(gè)共同的Function<R>,F(xiàn)unction<R>代表的正是方法類型。除此之外,每個(gè)方法類型都提供了一個(gè)包含有對(duì)應(yīng)參數(shù)個(gè)數(shù)的invoke操作符。
- 在m0方法中調(diào)用傳入的方法類型實(shí)例的時(shí)候,實(shí)際上就是通過(guò)方法的invoke來(lái)完成調(diào)用的,對(duì)應(yīng)的字節(jié)碼摘錄如下:
L1
LINENUMBER 3 L1
ALOAD 1
INVOKEINTERFACE
kotlin/jvm/functions/Function0.invoke ()Ljava/lang/Object;
POP
L2
有上述代碼可知,kotlin最終是通過(guò)Function0.invoke ()來(lái)完成了checkStr方法的調(diào)用。
- kotlin會(huì)為lambda表達(dá)式生成一個(gè)新類,類名為自己所處的類名(Test)+ 所處的方法名(test)+ 數(shù)字(從1開(kāi)始,有多個(gè)則依次遞增)。該類繼承了Lambda類并實(shí)現(xiàn)了對(duì)應(yīng)的Function接口。字節(jié)碼如下所示:
//生成的新類繼承了Lambda類,并實(shí)現(xiàn)了Function0接口
final class Test$test$2 extends kotlin/jvm/internal/Lambda implements kotlin/jvm/functions/Function0 {
Lambda類實(shí)際上沒(méi)有什么東西,就是提供了lambda參數(shù)數(shù)量的入口:getArity。而Function0正是我們上面提到的有invoke入口的接口。
- 當(dāng)調(diào)用方法m0的時(shí)候,傳入的lambda實(shí)例,實(shí)際上就是通過(guò)上述生成的新類實(shí)例完成的。而且實(shí)際調(diào)用的時(shí)候也正是通過(guò)上述生成的新類的invoke方法完成調(diào)用的,對(duì)應(yīng)的字節(jié)碼如下所示:
//test方法對(duì)應(yīng)的字節(jié)碼
// access flags 0x11
public final test()V
L0
LINENUMBER 7 L0
ALOAD 0
//這里實(shí)際上是調(diào)用了生成類LTest$test$1的實(shí)例
GETSTATIC Test$test$1.INSTANCE : LTest$test$1;
CHECKCAST kotlin/jvm/functions/Function0
INVOKEVIRTUAL Test.m0 (Lkotlin/jvm/functions/Function0;)V//將LTest$test$1實(shí)例傳入方法m0
L1
- 步驟4中提到的INSTANCE,實(shí)際上是kotlin為lambda表達(dá)式生成的新類、提供了一個(gè)單例對(duì)象。該單例對(duì)象是在該類的類構(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)用了類的實(shí)例構(gòu)造方法,即生成了一個(gè)Test$test$1對(duì)象
INVOKESPECIAL Test$test$1.<init> ()V
//這里完成了對(duì)INSTANCE的賦值,其值正式上面生成的新對(duì)象
PUTSTATIC Test$test$1.INSTANCE : LTest$test$1;
RETURN
MAXSTACK = 2
MAXLOCALS = 0
- 在kotlin為我們生成的Test$test$1類中的invoke方法中,正式完成了方法實(shí)例對(duì)應(yīng)的實(shí)現(xiàn),如下所示:
//這里就是invoke的實(shí)現(xiàn),lambda表達(dá)式實(shí)例對(duì)應(yīng)的實(shí)現(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)生完畢。下面看下匿名方法的實(shí)現(xiàn)原理。
匿名方法背后的原理
我們將上述代碼的調(diào)用改成匿名方法的實(shí)現(xiàn),如下所示:
//將m0方法的調(diào)用改成傳入匿名方法實(shí)例方式
fun test() {
m0(fun(): String { return "return a str" })
}
通過(guò)查看對(duì)應(yīng)的字節(jié)碼發(fā)現(xiàn),匿名方法和lambda表達(dá)式對(duì)應(yīng)的字節(jié)碼完全一樣,重要的字節(jié)碼摘錄如下所示:
//同樣生成了一個(gè)新類,同lambda表達(dá)式一致
final class Test$test$1 extends kotlin/jvm/internal/Lambda implements kotlin/jvm/functions/Function0 {
// 也提供了一個(gè)單例實(shí)現(xiàn)
public final static LTest$test$1; INSTANCE
//匿名方法的實(shí)現(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表達(dá)式實(shí)現(xiàn)一致。
閉包的訪問(wèn)原理
在上篇高階方法及l(fā)ambda表達(dá)式中,我們提到了閉包的概念,那么為什么lambda表達(dá)式和匿名方法能夠自由訪問(wèn)外部作用域中的成員呢?本小節(jié)來(lái)分析下原因。
首先附上我們要分析的源代碼:
class Test {
//為了更能說(shuō)服問(wèn)題,這里提供了一個(gè)private的私有屬性
private val str:String = "test"
fun m0(checkStr: () -> String) {
}
//測(cè)試方法,這里完成了對(duì)外部作用域的訪問(wèn),即$str
fun test() {
m0(fun(): String { return "return a $str" })
}
}
上述代碼生成的字節(jié)碼這里不再全部粘貼,直接撿重點(diǎn)的來(lái)看下:
- kotlin編譯器會(huì)為私有是成員str生成一個(gè)公有的訪問(wèn)方法,對(duì)應(yīng)字節(jié)碼如下所示:
//位于Test類中,由編譯器為我們自動(dòng)生成的訪問(wèn)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
- 在生成的新類中,會(huì)持有Test類的引用,并在實(shí)例構(gòu)造方法中將傳入的Test類對(duì)象賦值給了Test$test$1.this$0,對(duì)應(yīng)字節(jié)碼如下所示:
//位于生成的Test$test$1類中
<init>(LTest;)V
ALOAD 0
ALOAD 1
//這里實(shí)際上將傳入的Test對(duì)象賦值給了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對(duì)象,實(shí)際上是在test方法調(diào)用的時(shí)候生成的,對(duì)應(yīng)的字節(jié)碼如下所示:
//test方法對(duì)應(yīng)的字節(jié)碼
public final test()V
L0
LINENUMBER 7 L0
ALOAD 0
NEW Test$test$1//生成一個(gè)新的Test$test$1對(duì)象
DUP
ALOAD 0
//這句字節(jié)碼表示傳入了Test對(duì)象this
INVOKESPECIAL Test$test$1.<init> (LTest;)V
CHECKCAST kotlin/jvm/functions/Function0
INVOKEVIRTUAL Test.m0 (Lkotlin/jvm/functions/Function0;)V
- kotlin正式通過(guò)上述三個(gè)步驟完成了對(duì)閉包環(huán)境下的訪問(wèn),因?yàn)榉椒w實(shí)現(xiàn)都在新生成類中的invoke中,所以其對(duì)應(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;
//這里完成了對(duì)str的訪問(wèn)
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實(shí)例
GETFIELD Test$test$1.this$0 : LTest;
//這里通過(guò)生成的共有方法access$getStr$p 完成了對(duì)str的訪問(wèn)
INVOKESTATIC Test.access$getStr$p (LTest;)Ljava/lang/String;
至此,高階方法相關(guān)的原理分析完畢。