kotlin入门潜修之进阶篇—高阶方法和lambda表达式原理

本文收录于 kotlin入门潜修专题系列,欢迎学习交流。

创作不易,如有转载,还请备注。

高阶方法及lambda表达式原理

照例,先看下我们要分析的源代码片段。如下所示:

class Test {
//定义了一个高阶方法m0
    fun m0(checkStr: () -> String) {
        checkStr()
    }
//测试方法,用于测试m0
    fun test(){
        m0 {  -> "return a str" }//采用lambda表达式实例化方法类型
    }
}

一个最简单的高阶方法及lambda表达式的示例,来看下对应的字节码,照例先全部粘贴下:

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
}

字节码比较长,这里照例分割成几个部分来进行阐述。

  1. 高阶方法中的方法类型实际上会被编译成Function类型,在本例中的类型就是Function0,其对应的字节码如下所示:
  public final m0(Lkotlin/jvm/functions/Function0;)V

这就是kotlin中对方法类型的处理,那么是所有的方法类型都会被编译成Function0吗?当然不是,kotlin会根据方法类型的参数个数来做相应的处理,比如现在方法类型有1个入参,则就会被编译成Function1类型,有2个入参就会被编译成Function2类型,一次类推。下面我粘贴一个有两个入参的字节码编译案例:

//源代码m0,其入参是有两个参数的方法类型
    fun m0(checkStr: (str1:String, str2:String) -> String) {
    }
//编译后对应的字节码,从中可知,
//两个参数的方法类型被编译成了Function2类型
  public final m0(Lkotlin/jvm/functions/Function2;)V

事实上,我们不从字节码的角度也能发现这个规律,kotlin所有的方法类型都会被处理成Function家族类型,只不过参数个数不同,对应的具体的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个参数对应的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个入参的方法类型。这些方法都有一个共同的Function<R>,Function<R>代表的正是方法类型。除此之外,每个方法类型都提供了一个包含有对应参数个数的invoke操作符。

  1. 在m0方法中调用传入的方法类型实例的时候,实际上就是通过方法的invoke来完成调用的,对应的字节码摘录如下:
   L1
    LINENUMBER 3 L1
    ALOAD 1
    INVOKEINTERFACE 
kotlin/jvm/functions/Function0.invoke ()Ljava/lang/Object;
    POP
   L2

有上述代码可知,kotlin最终是通过Function0.invoke ()来完成了checkStr方法的调用。

  1. kotlin会为lambda表达式生成一个新类,类名为自己所处的类名(Test)+ 所处的方法名(test)+ 数字(从1开始,有多个则依次递增)。该类继承了Lambda类并实现了对应的Function接口。字节码如下所示:
//生成的新类继承了Lambda类,并实现了Function0接口
final class Test$test$2 extends kotlin/jvm/internal/Lambda  implements kotlin/jvm/functions/Function0  {

Lambda类实际上没有什么东西,就是提供了lambda参数数量的入口:getArity。而Function0正是我们上面提到的有invoke入口的接口。

  1. 当调用方法m0的时候,传入的lambda实例,实际上就是通过上述生成的新类实例完成的。而且实际调用的时候也正是通过上述生成的新类的invoke方法完成调用的,对应的字节码如下所示:
//test方法对应的字节码
// access flags 0x11
  public final test()V
   L0
    LINENUMBER 7 L0
    ALOAD 0
//这里实际上是调用了生成类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表达式生成的新类、提供了一个单例对象。该单例对象是在该类的类构造方法中完成初始化的,如下所示:
//本例中Test$test$1的成员属性INSTANCE
  public final static LTest$test$1; INSTANCE
//Test$test$1的类构造方法
  static <clinit>()V
    NEW Test$test$1
    DUP
//这里调用了类的实例构造方法,即生成了一个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方法中,正式完成了方法实例对应的实现,如下所示:
//这里就是invoke的实现,lambda表达式实例对应的实现
//就是在这里。
  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背后的原理产生完毕。下面看下匿名方法的实现原理。

匿名方法背后的原理

我们将上述代码的调用改成匿名方法的实现,如下所示:

//将m0方法的调用改成传入匿名方法实例方式
    fun test() {
        m0(fun(): String { return "return a str" })
    }

通过查看对应的字节码发现,匿名方法和lambda表达式对应的字节码完全一样,重要的字节码摘录如下所示:

//同样生成了一个新类,同lambda表达式一致
final class Test$test$1 extends kotlin/jvm/internal/Lambda  implements kotlin/jvm/functions/Function0  {
// 也提供了一个单例实现
public final static LTest$test$1; INSTANCE
//匿名方法的实现也在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
//等等...

上面省略了一些分析,最终总结一句话,匿名方法和lambda表达式实现一致。

闭包的访问原理

在上篇高阶方法及lambda表达式中,我们提到了闭包的概念,那么为什么lambda表达式和匿名方法能够自由访问外部作用域中的成员呢?本小节来分析下原因。
首先附上我们要分析的源代码:

class Test {
//为了更能说服问题,这里提供了一个private的私有属性
    private val str:String = "test"
    fun m0(checkStr: () -> String) {
    }
//测试方法,这里完成了对外部作用域的访问,即$str
    fun test() {
        m0(fun(): String { return "return a $str" })
    }
}

上述代码生成的字节码这里不再全部粘贴,直接捡重点的来看下:

  1. kotlin编译器会为私有是成员str生成一个公有的访问方法,对应字节码如下所示:
//位于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类的引用,并在实例构造方法中将传入的Test类对象赋值给了Test$test$1.this$0,对应字节码如下所示:
//位于生成的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方法调用的时候生成的,对应的字节码如下所示:
//test方法对应的字节码
  public final test()V
   L0
    LINENUMBER 7 L0
    ALOAD 0
    NEW Test$test$1//生成一个新的Test$test$1对象
    DUP
    ALOAD 0
//这句字节码表示传入了Test对象this
    INVOKESPECIAL Test$test$1.<init> (LTest;)V
    CHECKCAST kotlin/jvm/functions/Function0
    INVOKEVIRTUAL Test.m0 (Lkotlin/jvm/functions/Function0;)V
  1. kotlin正式通过上述三个步骤完成了对闭包环境下的访问,因为方法体实现都在新生成类中的invoke中,所以其对应的字节码如下所示:
  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;

至此,高阶方法相关的原理分析完毕。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341