從原理分析Kotlin的延遲初始化: lateinit var和by lazy

鹹魚不思議發表於2018-05-19

Koltin中屬性在宣告的同時也要求要被初始化,否則會報錯。 例如以下程式碼:

private var name0: String //報錯
private var name1: String = "xiaoming" //不報錯
private var name2: String? = null //不報錯
複製程式碼

  可是有的時候,我並不想宣告一個型別可空的物件,而且我也沒辦法在物件一宣告的時候就為它初始化,那麼這時就需要用到Kotlin提供的延遲初始化
  Kotlin中有兩種延遲初始化的方式。一種是lateinit var,一種是by lazy

lateinit var

private lateinit var name: String
複製程式碼

  lateinit var只能用來修飾類屬性,不能用來修飾區域性變數,並且只能用來修飾物件,不能用來修飾基本型別(因為基本型別的屬性在類載入後的準備階段都會被初始化為預設值)。
  lateinit var的作用也比較簡單,就是讓編譯期在檢查時不要因為屬性變數未被初始化而報錯。
  Kotlin相信當開發者顯式使用lateinit var 關鍵字的時候,他一定也會在後面某個合理的時機將該屬性物件初始化的(然而,誰知道呢,也許他用完才想起還沒初始化)。

by lazy

  by lazy本身是一種屬性委託。屬性委託的關鍵字是by。by lazy 的寫法如下:

//用於屬性延遲初始化
val name: Int by lazy { 1 }

//用於區域性變數延遲初始化
public fun foo() {
    val bar by lazy { "hello" }
    println(bar)
}
複製程式碼

  以下以name屬性為代表來講解by kazy的原理,區域性變數的初始化也是一樣的原理。
  by lazy要求屬性宣告為val,即不可變變數,在java中相當於被final修飾。
  這意味著該變數一旦初始化後就不允許再被修改值了(基本型別是值不能被修改,物件型別是引用不能被修改)。{}內的操作就是返回唯一一次初始化的結果。
  by lazy可以使用於類屬性或者區域性變數。

  寫一段最簡單的程式碼分析by lazy的實現:

class TestCase {

   private val name: Int by lazy { 1 }

   fun printname() {
       println(name)
   }

}
複製程式碼

  在IDEA中點選toolbar中的 Tools -> Kotlin -> Show Kotlin ByteCode, 檢視編輯器右側的工具欄:

檢視位元組碼
不想看位元組碼分析的可以直接跳過,每段位元組碼後面都有java/kotlin版本的解釋

更完整的位元組碼片段如下:

public <init>()V
 L0
  LINENUMBER 3 L0
  ALOAD 0
  INVOKESPECIAL java/lang/Object.<init> ()V
 L1
  LINENUMBER 5 L1
  ALOAD 0
  GETSTATIC com/rhythm7/bylazy/TestCase$name$2.INSTANCE : Lcom/rhythm7/bylazy/TestCase$name$2;
  CHECKCAST kotlin/jvm/functions/Function0
  INVOKESTATIC kotlin/LazyKt.lazy (Lkotlin/jvm/functions/Function0;)Lkotlin/Lazy;
  PUTname com/rhythm7/bylazy/TestCase.name$delegate : Lkotlin/Lazy;
  RETURN
 L2
  LOCALVARIABLE this Lcom/rhythm7/bylazy/TestCase; L0 L2 0
  MAXSTACK = 2
  MAXLOCALS = 1
複製程式碼

  該段程式碼是在位元組碼生成的public <clinit>()V 方法內的。之所以是在該方法內,是因為非單例object的Kotlin類的屬性初始化程式碼語句經過編譯器處理後都會被收集到該方法內,如果是object物件,對應的屬性初始化程式碼語句則會被收集到static <clinit>()V方法中。另外,在位元組碼中,這兩個方法是擁有不同方法簽名的,這與語言級別上判斷兩個方法是否相同的方式有所不同。前者是例項構造方法,後者是類構造方法。
  L0與L1之間的位元組碼代表呼叫了Object()的構造方法,這是預設的父類構造方法。L2之後的是本地變數表說明。L1與L2之間的位元組碼對應如下kotlin程式碼:

 private val name: Int by lazy { 1 }
複製程式碼

L1與L2之間這段位元組碼的意思是:
原始碼行號5對應位元組碼方法體內的行號1; 將this(非靜態方法預設的第一個本地變數)推送至棧頂;
獲取靜態變數com.rhythm7.bylazy.TestCase$name$2.INSTANCE;
檢驗INSTANCE能否轉換為kotlin.jvm.functions.Function0類;
呼叫靜態方法kotlin.LazyKt.lazy(kotlin.jvm.functions.Function0),將INSTANCE作為引數傳入,並獲得一個kotlin.Lazy型別的返回值;
將以上返回值賦值給com.rhythm7.bylazy.TestCase.name$delegate;
最後結束方法。

相當於java程式碼:

TestCase() {
    name$delegate = LazyKt.lazy((Function0)name$2.INSTANCE)
}
複製程式碼

其中name$delegate是編譯後生成的屬性,物件型別為Lazy。

  private final Lkotlin/Lazy; name$delegate  
複製程式碼

name$2都是編譯後生成的內部類。

final class com/rhythm7/bylazy/TestCase$name$2 extends kotlin/jvm/internal/Lambda  implements kotlin/jvm/functions/Function0
複製程式碼

  name$2繼承了kotlin.jvm.internal.Lambda類並實現了kotlin.jvm.functions.Function0介面, 可以看出name$2其實就是kotlin函式引數型別()->T的具體實現,通過位元組碼分析不難知道name$2.INSTANCE則是該實現類的一個靜態物件例項。
所以以上位元組碼又相當於Koltin中的:

init {
    name$delegate = lazy(()->{})
}
複製程式碼

  然而,這些程式碼的作用僅僅是給一個編譯期生成的屬性變數賦值而已,並沒有其他的操作。
  真正實現屬性變數延遲初始化的地方其實是在屬性name的getter方法裡。
  如果在java程式碼中呼叫過kotlin程式碼,會發現java程式碼中只能通過setter或getter的方式訪問koltin編寫的物件屬性,這是因為kotlin中預設會對屬性新增private修飾符,並根據該屬性變數是val還是var生成getter或getter和setter一起生成。然後又根據對該屬性的訪問許可權給getter和setter新增對應的訪問許可權修飾符(預設是public)。

檢視getName()的具體實現:

private final getName()I
 L0
  ALOAD 0
  GETFIELD com/rhythm7/bylazy/TestCase.name$delegate : Lkotlin/Lazy;
  ASTORE 1
  ALOAD 0
  ASTORE 2
  GETSTATIC com/rhythm7/bylazy/TestCase.$$delegatedProperties : [Lkotlin/reflect/KProperty;
  ICONST_0
  AALOAD
  ASTORE 3
 L1
  ALOAD 1
  INVOKEINTERFACE kotlin/Lazy.getValue ()Ljava/lang/Object;
 L2
  CHECKCAST java/lang/Number
  INVOKEVIRTUAL java/lang/Number.intValue ()I
  IRETURN
 L3
  LOCALVARIABLE this Lcom/rhythm7/bylazy/TestCase; L0 L3 0
  MAXSTACK = 2
  MAXLOCALS = 4
複製程式碼

相當於java程式碼:

private final int getName(){
    Lazy var1 = this.name$delegate;
    KProperty var2 = this.$$delegatedProperties[0]
    return ((Number)var1.getValue()).intValue()
}
複製程式碼

  可以看到name的getter方法其實是返回了 name$delegate.getValue()方法。$$delegatedProperties是編譯後自動生成的屬性,但在此處並沒有用到,所以不用關心。

  那麼現在我們要關心的就只有name$delegate.getValue(),也就是Lazy類getValue()方法的具體實現了。

先看LazyKt.lazy(()->T)的實現:

public fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
複製程式碼

再看SynchronizedLazyImpl類的具體實現:

private object UNINITIALIZED_VALUE

private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE
    // final field is required to enable safe publication of constructed instance
    private val lock = lock ?: this

    override val value: T
        get() {
            val _v1 = _value
            if (_v1 !== UNINITIALIZED_VALUE) {
                @Suppress("UNCHECKED_CAST")
                return _v1 as T
            }

            return synchronized(lock) {
                val _v2 = _value
                if (_v2 !== UNINITIALIZED_VALUE) {
                    @Suppress("UNCHECKED_CAST") (_v2 as T)
                }
                else {
                    val typedValue = initializer!!()
                    _value = typedValue
                    initializer = null
                    typedValue
                }
            }
        }

......
}
複製程式碼

  以上程式碼的閱讀難度就非常低了。
  SynchronizedLazyImpl繼承了Lazy類,並指定了泛型型別,然後重寫了Lazy父類的getValue()方法。 getValue()方法中會對_value是否已初始化做判斷,並返回_value,從而實現value的延遲初始化的作用。
  注意,對value的初始化行為本身是執行緒安全的。

總結

  總結一下,當一個屬性name需要by lazy時,具體是怎麼實現的:

  1. 生成一個該屬性的附加屬性:name$$delegate;
  2. 在構造器中,將使用lazy(()->T)建立的Lazy例項物件賦值給name$$delegate;
  3. 當該屬性被呼叫,即其getter方法被呼叫時返回name$$delegate.getVaule(),而name$$delegate.getVaule()方法的返回結果是物件name$$delegate內部的_value屬性值,在getVaule()第一次被呼叫時會將_value進行初始化,往後都是直接將_value的值返回,從而實現屬性值的唯一一次初始化。

那麼,再總結一下,lateinit var和by lazy哪個更好用?
  首先兩者的應用場景是略有不同的。
  然後,雖然兩者都可以推遲屬性初始化的時間,但是lateinit var只是讓編譯期忽略對屬性未初始化的檢查,後續在哪裡以及何時初始化還需要開發者自己決定。
  而by lazy真正做到了宣告的同時也指定了延遲初始化時的行為,在屬性被第一次被使用的時候能自動初始化。但這些功能是要為此付出一丟丟代價的。

相關文章