kotlin入門潛修之進階篇—高階方法和lambda表達式原理

本文收錄于 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é)碼比較長仿吞,這里照例分割成幾個部分來進行闡述。

  1. 高階方法中的方法類型實際上會被編譯成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操作符。

  1. 在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)用李滴。

  1. 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入口的接口。

  1. 當(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
  1. 步驟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
  1. 在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é)碼這里不再全部粘貼膏燕,直接撿重點的來看下:

  1. 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
  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
  1. 步驟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
  1. 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)的原理分析完畢击纬。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市钾麸,隨后出現(xiàn)的幾起案子更振,更是在濱河造成了極大的恐慌,老刑警劉巖饭尝,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件肯腕,死亡現(xiàn)場離奇詭異,居然都是意外死亡钥平,警方通過查閱死者的電腦和手機实撒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來涉瘾,“玉大人知态,你說我怎么就攤上這事×⑴眩” “怎么了负敏?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長秘蛇。 經(jīng)常有香客問我原在,道長友扰,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任庶柿,我火速辦了婚禮村怪,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘浮庐。我一直安慰自己甚负,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布审残。 她就那樣靜靜地躺著梭域,像睡著了一般。 火紅的嫁衣襯著肌膚如雪搅轿。 梳的紋絲不亂的頭發(fā)上病涨,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天,我揣著相機與錄音璧坟,去河邊找鬼既穆。 笑死,一個胖子當(dāng)著我的面吹牛雀鹃,可吹牛的內(nèi)容都是我干的幻工。 我是一名探鬼主播,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼黎茎,長吁一口氣:“原來是場噩夢啊……” “哼囊颅!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起傅瞻,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤踢代,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后嗅骄,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體奸鬓,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年掸读,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片宏多。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡儿惫,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出伸但,到底是詐尸還是另有隱情肾请,我是刑警寧澤,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布更胖,位于F島的核電站铛铁,受9級特大地震影響隔显,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜饵逐,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一括眠、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧倍权,春花似錦掷豺、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至默辨,卻和暖如春德频,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背缩幸。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工壹置, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人桌粉。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓蒸绩,卻偏偏與公主長得像,于是被迫代替她去往敵國和親铃肯。 傳聞我的和親對象是個殘疾皇子患亿,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,044評論 2 355