Kotlin中l(wèi)ateinit變量在字節(jié)碼層面上的解釋

概述

在Kotlin里面,變量可以聲明為late init:

lateinit var str: String

顧名思義,這是指一個(gè)延遲初始化的變量。在kotlin里面,如果在類型聲明之后沒有使用符號?,則表示該變量不會(huì)為null。但是這個(gè)時(shí)候會(huì)要求我們初始化一個(gè)值。有些時(shí)候,我們在聲明變量的時(shí)候,并不能初始化這個(gè)變量。比如說在使用Spring的時(shí)候,我們會(huì)聲明一個(gè)變量,但是在afterProperties里面進(jìn)行初始化。

但是我們又想使用kotlin非null變量帶來的便利,這個(gè)時(shí)候,你需要的就是lateinit了。它告訴編譯器,這個(gè)變量會(huì)被初始化,并且不會(huì)為null,但是在聲明這里,我暫時(shí)還不知道什么時(shí)候會(huì)被初始化。

編譯器知道你的變量沒有初始化嗎?

我們來考慮的第一個(gè)問題是,一個(gè)聲明成lateinit的變量,如果在整個(gè)代碼里面都沒有進(jìn)行任何的初始化,那么能否編譯通過?


代碼

答案是可以。所以,也就是在編譯層面上,kotlin的編譯器不會(huì)做這種檢查。如果你將變量聲明為lateinit,它就認(rèn)為你肯定會(huì)初始化,至于你是怎么初始化它的,它就不管了。

如果一個(gè)變量聲明為lateinit,但是沒有初始化,而又被使用了的話,會(huì)拋出一個(gè)異常UninitializedPropertyAccessException。

那么問題來了,它究竟是怎么實(shí)現(xiàn)的呢?

lateinit變量

我們將剛才那段代碼的字節(jié)碼使用javap指令解析之后,看到str變量的內(nèi)容是:


str字節(jié)碼

在變量聲明的地方,可以看到它和普通的Java里面聲明的變量沒太多不同,比較大的一個(gè)不同是,它被編譯器添加了一個(gè)RuntimeInvisibleAnnotations。RuntimeInvisibleAnnotations表明這個(gè)注解在運(yùn)行時(shí)不能被訪問,這主要是指代碼層面無法訪問。因?yàn)檫@個(gè)RuntimeInvisibleAnnotations,也就是NotNull其聲明是:

NotNull注解

Rentention被設(shè)定成CLASS。

每次訪問都判斷是否初始化?

我在第一次使用的lateinit的時(shí)候就有這個(gè)疑問。因?yàn)橐粋€(gè)lateinit變量被初始化,卻被使用了,拋出來的異常是UninitializedPropertyAccessException,這個(gè)是kotlin自定義的異常,而不是JDK的異常。這是一個(gè)很關(guān)鍵的地方。如果是JDK的異常,那可能是JVM自身內(nèi)部檢測變量是否初始化了。

但是這個(gè)異常是Kotlin的,所以必然是Kotlin自己做了手腳。而這種手腳,或者黑科技,只能是通過編譯器在編譯期間插入字節(jié)碼指令來完成的。

我首先將懷疑的目光放到了getStr()方法上。我懷疑,Kotlin在代碼每次訪問str變量的時(shí)候,實(shí)際上替換成了getStr()方法,而后在getStr()里面完成這種校驗(yàn),類似于

    fun getStr(): String {
        if (str == null) {
            throw UninitializedPropertyAccessException()
        }
    }

但是這有一個(gè)很大的問題,首先,lateinit的確有可能被初始化為null,即便聲明為String而不是String?。那么這種改寫就是一個(gè)很奇怪的東西了。因?yàn)橐粋€(gè)沒有聲明為?的變量,卻是null,在被使用的時(shí)候,拋出來的異常是KotlinNullPointerException.這種改寫會(huì)導(dǎo)致語言設(shè)計(jì)層面上的不一致性。

使用反射就能做到這一點(diǎn),更加猥瑣的是利用本地方法調(diào)用。

另外一種考慮則是,這種方法簡直太消耗性能了。每次訪問一個(gè)變量,變成一個(gè)方法調(diào)用,效率至少慢一個(gè)數(shù)量級。

所以,就需要考慮JVM的機(jī)制了。我的第二個(gè)猜測是,在訪問的變量的那個(gè)地方,插入字節(jié)碼指令,檢測是否為null。JVM規(guī)范里面定義了一個(gè)字節(jié)碼指令ifnonnull,用于檢測一個(gè)變量是否為null:


ifnonnull指令

在查看javap之后的結(jié)果,果然是利用了這個(gè)指令:


sayHello字節(jié)碼

紅色箭頭指向的就是ifnonnull指令,其后跟著的13是指如果ifnonnull檢測為true,那么就會(huì)執(zhí)行標(biāo)記為13的字節(jié)碼指令,也就是astore_1。這條指令之后就是print(str)的過程。

可以看到,所謂的print()方法,也不過是一個(gè)語法糖而已,讀者可以自己去看看

而如果ifnonnull檢測失敗,接下來就是執(zhí)行藍(lán)色箭頭指向的指令,它暴露了拋出UninitializedPropertyAccessException的秘密,那就是,調(diào)用了kotlin/jvm/internal/Intrinsics.throwUninitializedPropertyAccessException:(Ljava/lang/String;)V


throwUninitializedPropertyAccessException

所以事實(shí)上到這里lateinit的秘密就已經(jīng)清楚了,無非就是編譯器給你插入檢測為null的指令而已。那么問題還是存在的,前面我已經(jīng)說過,為null并不代表沒有被初始化。但是如果結(jié)合Kotlin的語法,如果一個(gè)變量的類型聲明沒有使用?符號,則Kotlin認(rèn)為,這個(gè)變量被初始化之后必不能為null。所以用null檢測來取代是否初始化檢測,是符合Kotlin的設(shè)計(jì)的。

但是,基于JVM的語言都有的一個(gè)通病就是,如果你調(diào)用別的同樣JVM的語言的API,那么就會(huì)破壞自身的完整性和一致性。比如我說過用反射或者本地方法調(diào)用都能把lateinit變量初始化為null,實(shí)際上,這就已經(jīng)超出了kotlin的預(yù)計(jì)了,因此也就是破壞了kotlin自身的一致性。這個(gè)時(shí)候,出現(xiàn)語義前后不一致是很容易理解的。

如果再從JVM角度來討論一下,那就是,JVM本身也沒有提供變量是否初始化的指令。而且,JVM明確要求,在給變量分配內(nèi)存的時(shí)候,內(nèi)存應(yīng)該被“初始化”了,也就是全部比特位都置為0(這就是各個(gè)成員變量默認(rèn)值的來源)。也就是意味著,在聲明lateinit變量的時(shí)候,它的引用(指針?)已經(jīng)被置為null了。所以想達(dá)到真的檢測變量有沒有被用戶主動(dòng)初始化,基本上是不能依賴于JVM的機(jī)制,而只能依賴于編譯器。

很可惜,我最開始就說了,編譯器也放棄了檢測lateinit究竟有沒有被用戶主動(dòng)初始化。

我覺得是因?yàn)榫幾g器不能。或者說,代價(jià)太大而限制太多。

我覺得從這里只能得出的一個(gè)結(jié)論是:如果你的代碼真的顯示初始化了lateinit變量,而又拋出了UninitializedPropertyAccessException異常,并不需要驚訝,只是因?yàn)槟闱『脤⒆兞砍跏蓟癁閚ull了而已

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容