注册

关于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
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册