lateinit实现原理

  • lateinit就变量定义上于普通变量一致
  • lateinit是将变量的初始化延后,也就是说可以不立即对变量进行初始化,他具体的实现是通过对方法的get以及调用处进行值类型的判空处理
1
2
3
4
5
6
7
8
9
10
11
12
fun main() {
val test = Test()
test.cur
}

class Test {
lateinit var cur: String

fun printCur() {
println(cur)
}
}

就事论事分析main函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
L0
LINENUMBER 8 L0 #源代码行号标记
NEW Test #new Test类的实例压入操作数栈
DUP #复制栈顶元素,方便后续invoke消耗
INVOKESPECIAL Test.<init> ()V #弹栈顶元素,初始化对象
ASTORE 0 #元素存入局部变量表
L1
LINENUMBER 9 L1 #..
ALOAD 0 #局部变量表to操作数栈
INVOKEVIRTUAL Test.getCur ()Ljava/lang/String; #调用get方法
POP #弹栈(没有使用。
L2
LINENUMBER 10 L2
RETURN #方法返回

可以发现特别的中规中矩

lateinit定义分析

1
public Ljava/lang/String; cur

getCur

1
2
3
4
5
6
7
8
9
10
11
12
13
L0
LINENUMBER 13 L0 #标记行号
ALOAD 0 #加载局部变量表为0的位置(this)
GETFIELD Test.cur : Ljava/lang/String; #获取元素值
DUP #复制栈顶元素
IFNONNULL L1 #判断值是否为null
LDC "cur" #为空就将常量池种cur入栈
INVOKESTATIC kotlin/jvm/internal/Intrinsics.throwUninitializedPropertyAccessException (Ljava/lang/String;)V
#调用内部的静态方法
L1
ARETURN
#返回

所以等价的代码也就是

1
2
3
4
5
6
fun getCur():String{
if(this.cur == null){
Intrinsics.throwUninitializedPropertyAccessException("cur")
}
return cur
}

setCur

1
2
3
4
5
6
7
8
9
10
11
L0
ALOAD 1 #加载局部变量(方法传入的参数
LDC "<set-?>" #压入常量池的常量
INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V
#检测函数的参数
L1
LINENUMBER 13 L1
ALOAD 0 #加入this
ALOAD 1 #加入方法参数
PUTFIELD Test.cur : Ljava/lang/String; #设置值
RETURN #return;

代码逻辑没有发生变化

printCur

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public final printCur()V
L0
LINENUMBER 16 L0
ALOAD 0 #压入this
GETFIELD Test.cur : Ljava/lang/String; #获取cur的值
DUP #复制值
IFNONNULL L1 #判空
LDC "cur" #压入常量
INVOKESTATIC kotlin/jvm/internal/Intrinsics.throwUninitializedPropertyAccessException (Ljava/lang/String;)V
#如果为空就throw
L1
ASTORE 1
#非空存入局部变量表
L2
#这边就不分析了就System.out.println(cur)
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
ALOAD 1
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
L3
L4
LINENUMBER 17 L4
RETURN #return
L5
LOCALVARIABLE this LTest; L0 L5 0
MAXSTACK = 2
MAXLOCALS = 2

可以发现lateinit对于内部的调用出自动加入了判空处理

等价于

1
2
3
4
if(this.cur == null){
Intrinsics.throwUninitializedPropertyAccessException("cur")
}
System.out.println(cur)

小结

lateinit的实现比较简单。

  • 首先依托于kotlin的属性获取,外部对象访问内部对象是通过使用getter方法获取的,即使你的属性是public,他最后生成的代码也不会贸然调用getfield指令。
  • 在上述的确保下,只要能保证成员内部调用是安全的,外部就自然而然能确保
  • 而确保内部获取属性一定为非空很简单,就是对内部的获取属性的位置进行assert,一旦你获取属性就自行生成一段Intrinsics.throwUninitializedPropertyAccessException这样就确保了值一定非空

最后可以发现其实lateinit和java的普通变量是类似的,要说不同的话lateinit是fail-fast风格的,一旦调用就会assert。
lateinit没有使得kotlin是null安全的,使用不当还是会造成异常,只不过从null pointer转化到了UninitializedPropertyAccessException,所以还是需要慎用。

相对而言声明可空类型要安全不少,不过呢判空处理很是让人头大,开发者应该根据自己的需要自行判断。

  • 如果能确保这个类一定不为空,在定义成员变量的时候可以声明为lateinit来延时初始化.

  • 如果不可以确保是否为null那最好还是声明为可空类型,通过?. ?: !!操作符判断