关于Kotlin的一些小事
一、碎碎念
说实话,原本是没有这个系列的,或者说是没想过去建立这个系列。
虽然,但是,所以就有了(别问为什么?)
val var 声明变量
- 被 val 修饰的变量:被 final 修饰,且只会为其提供 getter() 而不会提供 setter() 方法。
- 因为被 final 修饰的值,只能被赋值一次;所以不会有 setter()。
- 是否添加了"?":声明变量的时候会根据是否有"?",将变量添加 NotNull 或者 Nullable 注解。
- 被 var 修饰的变量:普通定义变量的方式,且会同时提供 setter()、getter() 方法。
- 是否添加了"?":如果没有?,则setter()方法的入参会被标记位NotNull;如果有?,则setter()方法的入参会被标记为Nullable。
?. 操作符
- 对于声明为 var 的变量,在调用方法时会需要加上 ?. 操作符来进行判空处理,避免空指针。实现空安全。
- 实现原理:通过在方法内部构造一个局部变量,然后赋值为该数据,紧接着通过判断局部变量是否为空?如果为空,则进行预设的处理;如果不为空,则直接进行方法调用。
声明变量的方式,能否全部声明为可空来避免空指针?为什么?
- 猜测:这里涉及到一个 java 和 Kt 互调的问题。
- 假设1:【Java 调 Kotlin 方法,在于调用】java 用一个可能为空的数据作为方法参数去调用 kt 方法,如果此时入参为空,但 kt 方法将方法参数配置为不可空的数据类型,那么此时就会直接报空指针异常。
- 因为 kt 会对那些入参不可空的对象先进行空指针判断再执行方法操作。
- 假设2:【Kotlin 调 Java 方法,在于接收】kt 用一个不可空的变量来接收 java 方法调用得到的返回值,如果此时 java 方法返回一个空,那么此时就会直接报空指针异常。
单例的实现方式
- 后面新建文章再说
data class
- data class,编译之后变成 public final class;声明的所有参数会作为构造函数的入参。
- ① 声明为 val 的参数,只会被提供 getter() 方法;而声明为 var 的参数,会被同时提供 setter()/gettter() 方法。
- ② 带了 ? 标记的参数,即标明为可空的参数,在构造函数中会被检测是否为空并抛出异常。
by lazy 和 lateinit var
- 【作用对象不同】
- lateinit 只能用在 var 声明变量且数据类型不能为空。
- by lazy {} 只能用在 val 声明变量。
- 【初始化数据的时机不同】
- 使用 lateinit 标记的变量,认定了开发者自己在使用该变量之前一定会先为其赋值,所以在访问的时候,会先进行判空处理。如果为空则直接crash。
- 这也证实了 lateinit 只能对数据类型不为空的变量进行修饰。
- 通过 by lazy 声明的变量,会为该变量提供私有的 getter() 方法并通过该方法来访问变量,而真正保存数据的位置,是类中一个声明为 final 的数据类型为 Lazy 的私有代理对象,将其作为访问入口,通过 Lazy 的 value 属性来获取数据。Lazy.getValue() 会通过执行初始化函数 initializer 来进行初始化。
- 详见:链接,下面会接着说。
- 其他方面:看一下对比
by lazy - val
by lazy{} 的使用
- by lazy{} 入参:需要传入一个初始化数据的函数 initializer: () -> T。
- by lazy{} 返回值:会通过 initializer 函数作为方法参数,构造并返回一个 SynchronizedLazyImpl:Lazy 对象。
如何获取数据?
- 可见,访问 by lazy 的变量,会通过其 getter() 方法来获取数据。
- 而此时可以看到 getter() 方法是通过访问数据类型为 Lazy 的代理对象的 getValue() 方法获取数据;由上述可知,此时得到的代理对象是一个 SynchronizedLazyImpl:Lazy 对象。
- 立下一个 Flag:后续再对所有 Lazy 实现类新建文章看看?
SynchronizedLazyImpl:Lazy
- 【关于数据 _value:Any? 】
- 初始值为一个单例对象 internal object UNINITIALIZED_VALUE,表示当前未初始化。
- 因此 _value 的数据类型为 Any。
- 【getVaule() 方法】
- ① 首先会判断当前保存的数据 _value是否为这个单例对象 UNINITIALIZED_VALUE?如果不是,则直接通过 as 强装为返回值类型并返回。如果数据未初始化,那么
- ② 进入一个同步块 synchronized(lock),在同步块中,再次判断 _value是否为这个单例对象 UNINITIALIZED_VALUE?这里的流程就类似于 double check lock。如果已经初始化,则同样通过 as 强装为返回值类型并返回。如果数据未初始化,那么
- ③ 执行初始化函数 initializer 获取数据并赋值给 _value,从而保证下次获取数据时直接返回该数据。此时,还会将初始化函数 initializer 置空。然后返回数据。
- 【关于 initializer 函数】
- 由上述可见,我们传递给 lazy 的 Lambda ,会被编译成为一个静态内部类。
- 静态内部类:继承了 FunctionN,且是一个单例类。invoke() 方法的方法体就是我们在 lambda 中的操作,并且返回值为最后一句。
- 因此可以知道,在执行初始化函数的时候,实际上就是执行我们传递给 lazy{} 的 lambda 中的执行指令。
- 【关于线程安全】
- SynchronizedLazyImpl 接受一个锁对象 lock:Any?=null ,这个锁对象可以是任意类型的对象,当然也可以为空,那么默认使用的就是当前实例对象作为锁对象来进行加锁。
- 在执行初始化函数 initializer 为数据赋值的时候,正是通过加锁来保证线程安全。
lateinit var 对比 by lazy
- 关于线程安全
- by lazy {} 的初始化默认是线程安全的,默认是 SynchronizedLazyImpl:Lazy 实现。并且能保证初始化函数 initializer 只会被调用一次,在数据未初始化时进行调用 且 调用完毕后会置空。
- lateint 默认是不保证线程安全的。
- 关于内存泄漏
- 由上述可知,传递给 lazy 的 Lambda ,会被编译成为一个静态内部类。
- 在使用 by lazy{} 的时候,如果在 lambda 里面使用了类中的成员变量,那么这个引用会一直被持有,直到该初始化函数执行,即该变量被初始化了才会释放(因为初始化函数执行完毕之后会被置空,断开引用链)。
- 而这里就很可能会导致内存泄漏。
二、各种函数?
- Flag 立下来:
- T.let
- T.run
- T.also
- T.apply
- with
- run
- 扩展函数
- 高阶函数
- inline noinline crossinline
作者:冰美式上瘾患者
链接:https://juejin.cn/post/7085965272510627877
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。