kotlin—内联类及其原理

1、什么是内联类?

内联类是一个对另一个类进行包装的类,既然是对其它类的包装,那么它有什么特别之处,值得kotlin使用专门的语法来支持?使用上内联确实像是普通的包装类一样,但是在使用时所创建的内联类对象经过编译之后,在运行时不会创建真正的内联类对象而是返回被包装的类实例对象,在用到内联类对象的地方都会被编译为使用内联类内部的静态方法。
对其它类进行包装时,优先考虑时候内联类,它可以避免创建包装类对象,减少内存提高性能。

2、语法

内联类的声明语法只在class 前面加inline 进行修饰, 并且提供一个主构造函数参数是被包装的类对象,语法如下所示:

inline class InlineClassName(val otherClass: ClassType) [:superInterfaceList]{
  [override list]
  [params/funs]
}

内联类还是有不是限制的:

  • 主构造器只能有一个参数,且参数为被修饰的类对象
  • 不能继承类,但可以继承接口
  • 内联类不可被继承
  • 不能有init语句块
  • 不能有幕后字段,所以内联类只能有简单的计算属性(不能包含延迟属性/委托属性)

使用内联类跟普通类一样:

val inlineObj = InlineClass(otherClassObj)
inlineObj.param
inlineObj.fun(xxx)

3、原理

使用内联类看起来与普通类一样,只是声明时多了个inline的修饰,怎么能体现出运行时不会创建包装类而是返回被包装的类实例呢?
我们以一个案例及字节码进行分析:
使用内联类的源码如下:

inline class TInline(val str: String) {
    val a
        get() = 123
    val length: Int
        get() = str.length
}

class TInlineClient() {
    fun main() {
        val obj = TInline("abc")
        val sVal = "a = ${obj.a}, length = ${obj.length}, val = ${obj.str}"
        println("sVal = $obj")
    }
}

TInline类经过编译后的关键字节码文件如下:

//内联类被final修饰,表示不能被继承
public final class com/java/test/kt/TInline {

  // compiled from: TInline.kt
  //省略....
  // access flags 0x12
  //主构造函数中的参数即被包装的类String对象str
  private final Ljava/lang/String; str
  @Lorg/jetbrains/annotations/NotNull;() // invisible

  // access flags 0x11
  //自动生成被包装类对象的get方法,返回被包装类对象本身
  public final getStr()Ljava/lang/String;
  @Lorg/jetbrains/annotations/NotNull;() // invisible
   L0
    LINENUMBER 3 L0
    ALOAD 0 //将第1个变量,即this载入栈顶
    GETFIELD com/java/test/kt/TInline.str : Ljava/lang/String; //获取内联类的str字段,等价于this.str
    ARETURN //返回栈顶的值
   //省略....

  // access flags 0x1002
  //自动生成init方法
  private synthetic <init>(Ljava/lang/String;)V
    // annotable parameter count: 1 (invisible)
    @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
   L0
    ALOAD 1 //将第2个变量即方法的参数载入栈顶
    LDC "str" //常量池中的str的值
    //校验str对象不能为空
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
   L1
    LINENUMBER 3 L1
    ALOAD 0 //将第1个变量即this载入栈顶
    //调用父类的init方法,即Object的init方法
    INVOKESPECIAL java/lang/Object.<init> ()V
    //给str赋值为常量池中str的值
    ALOAD 0
    ALOAD 1
    PUTFIELD com/java/test/kt/TInline.str : Ljava/lang/String;
    RETURN
   //省略....

  // access flags 0x19
  //将属性a转为getA-impl静态方法,返回的是a的值
  public final static getA-impl(Ljava/lang/String;)I
   L0
    LINENUMBER 5 L0
    //将a的get值123压入栈顶
    BIPUSH 123
    IRETURN //返回栈顶的值即123
  //省略....

  // access flags 0x19
  //将属性length转为getLength-impl静态方法,返回的是length的get值
  public final static getLength-impl(Ljava/lang/String;)I
   L0
    LINENUMBER 7 L0
    ALOAD 0 //载入str到栈顶
    INVOKEVIRTUAL java/lang/String.length ()I //调用栈顶str对象的length方法,并把值压入栈顶
    IRETURN  //返回栈顶str的length()值
   //省略....

  // access flags 0x9
  //虚拟构造函数的静态实现方法,参数为str对象
  public static constructor-impl(Ljava/lang/String;)Ljava/lang/String;
  //省略....
   L0
    ALOAD 0 //将第一个变量即方法的第1个参数str载入栈顶
    LDC "str" //从常量池中取str放入栈顶
    //校验栈顶对象即str不能为空
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
   L1
    LINENUMBER 3 L1
    ALOAD 0  //将第一个变量即方法的第1个参数str载入栈顶
    ARETURN //返回栈顶即str对象
   //省略....

  //省略....

  // access flags 0x9
  //内联对象转为字符串的静态方法
  public static toString-impl(Ljava/lang/String;)Ljava/lang/String;
  //省略....
    //新建StringBuilder对象
    NEW java/lang/StringBuilder
    DUP
    //调用StringBuilder的init方法
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    //StringBuilder对象添加字符串
    LDC "TInline(str="
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    //将第1个局部变量即str载入栈顶
    ALOAD 0
   //StringBuilder添加栈顶str的值
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    //从常量池中载入")"
    LDC ")"
    //StringBuilder添加栈顶")"的值
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;  
    //调用StringBuilder的toString 方法,并把返回值放入栈顶
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    ARETURN //返回栈顶的值
   //省略....

    //省略....
}

内联类TInline编译之后可以看到:

  • 内联类内部的属性编译之后变成静态方法的getXXX-impl方法,返回的是属性的值
  • 内联类内部生成一个constructor-impl静态方法,返回的是被包装类即str本身

我们接着看看使用内联类的TInlineClient编译之后发生了什么:

// class version 52.0 (52)
// access flags 0x31
public final class com/java/test/kt/TInlineClient {

  // compiled from: TInline.kt
  //省略....

  // access flags 0x11
  public final main()V
   L0
    LINENUMBER 12 L0
    LDC "abc" //将常量池中的"abc"载入栈顶
    //调用TInline的constructor-impl方法,参数为栈顶abc的字符串,并把返回值放入栈顶
    INVOKESTATIC com/java/test/kt/TInline.constructor-impl (Ljava/lang/String;)Ljava/lang/String;
    //把栈顶的值赋值给第2个局部变量obj
    ASTORE 1
   L1
    LINENUMBER 13 L1
    //创建StringBuilder对象
    NEW java/lang/StringBuilder
    DUP
    //调用StringBuilder的init方法
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    LDC "a = "  //将常量池中的"a = "载入栈顶
    //调用StringBuilder的append方法拼接栈顶"a = "字符串
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 1 //载入第2个局部变量即obj到栈顶
    //调用TInline.getA-impl 方法,栈顶即obj作为参数,并将结果放入栈顶
    INVOKESTATIC com/java/test/kt/TInline.getA-impl (Ljava/lang/String;)I
    //调用StringBuilder的append方法拼接栈顶的值
    INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
    LDC ", length = " //将常量池中的", length = "载入栈顶
     //调用StringBuilder的append方法拼接栈顶的值
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 1  //载入第2个局部变量即obj到栈顶
    //调用TInline.getLength-impl 方法,栈顶即obj作为参数,并将结果放入栈顶
    INVOKESTATIC com/java/test/kt/TInline.getLength-impl (Ljava/lang/String;)I
    //调用StringBuilder的append方法拼接栈顶的值
    INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
    LDC ", val = " //将常量池中的", val = " 载入栈顶
    //调用StringBuilder的append方法拼接栈顶的值
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 1  //载入第2个局部变量即obj到栈顶
    //调用StringBuilder的append方法拼接栈顶obj的值,obj本身是String
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
     //调用StringBuilder的toString 方法,并将返回值放入栈顶
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    ASTORE 2  //将栈顶的值赋值给第3个局部变量即sVal
   //省略....
}

TInlineClient编译之后,使用内联类TInline的地方都转为调用TInline的静态方法,创建TInline对象的地方变成了调用TInline的constructor-impl静态方法,参数是被包装的类,TInline的constructor-impl返回的是其实就是被包装类本身,使用TInline的属性/方法的地方都变为调用其内部的静态方法。

总结:
内联类在编译之后,会自动生成属性/方法/被包装类的静态方法,在使用时都是转为调用对应的静态方法,创建内联类的地方也会变成调用constructor-impl静态方法,返回的是被包装类本身。所以内联类提升包装类的性能,因为它不会创建真实的内联类对象,而是返回被包装类本身。

在使用包装类的场景可以考虑使用内联类实现,它可以提示包装类的性能。

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

推荐阅读更多精彩内容