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

Koltin中屬性在聲明的同時也要求要被初始化,不然會報錯。 例如如下代碼:java

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 關鍵字的時候,他必定也會在後面某個合理的時機將該屬性對象初始化的(然而,誰知道呢,也許他用完纔想起還沒初始化)。bash

by lazy

  by lazy自己是一種屬性委託。屬性委託的關鍵字是by。by lazy 的寫法以下:markdown

//用於屬性延遲初始化
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可使用於類屬性或者局部變量。jvm

  寫一段最簡單的代碼分析by lazy的實現:編輯器

class TestCase {

   private val name: Int by lazy { 1 }

   fun printname() {
       println(name)
   }

}
複製代碼

  在IDEA中點擊toolbar中的 Tools -> Kotlin -> Show Kotlin ByteCode, 查看編輯器右側的工具欄:
ide

查看字節碼
不想看字節碼分析的能夠直接跳過,每段字節碼後面都有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;
最後結束方法。ui

至關於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真正作到了聲明的同時也指定了延遲初始化時的行爲,在屬性被第一次被使用的時候能自動初始化。但這些功能是要爲此付出一丟丟代價的。

相關文章
相關標籤/搜索