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静态方法,返回的是被包装类本身。所以内联类提升包装类的性能,因为它不会创建真实的内联类对象,而是返回被包装类本身。
在使用包装类的场景可以考虑使用内联类实现,它可以提示包装类的性能。