前言
這里是專欄 重學(xué) Kotlin ,靈感來自于 Medium 上 Android Developers 團(tuán)隊(duì)的 Kotlin Vocabulary 。
作為一名 Kotlin 老鐵粉,我可能在博客里不止一次的表達(dá)過對(duì) Kotlin 的態(tài)度。
都 2020 了,作為一名安卓開發(fā)者,再不會(huì) Kotlin ,真的說不過去了!
介紹 Kotlin 語(yǔ)法的文章很多,那么,在這個(gè)系列中,我會(huì)寫一些什么呢?
Kotlin 再?gòu)?qiáng)大,也逃脫不了在 JVM 上運(yùn)行。經(jīng)過 kotlinc
編譯之后,生成的依舊是 .class
文件。
所以,學(xué)習(xí) Kotlin 的最佳方式其實(shí)就是查看字節(jié)碼。Android Studio 直接提供了插件,按如下方式即可查看:
Tools -> Kotlin -> Show Kotlin Bytecode
當(dāng)然,字節(jié)碼可讀性太差,IDE 提供了 Decompile ,將字節(jié)碼轉(zhuǎn)換成 Java 代碼。
這樣,我們就可以輕松掌握 Kotlin 各種語(yǔ)法的本質(zhì)。
本系列的每一篇文章都會(huì)選擇一個(gè)關(guān)鍵字或者知識(shí)點(diǎn),剖析本質(zhì),幫助大家快速深入理解 Kotlin 。
下面就進(jìn)入今天的主角 object 。
目錄
object 有哪些用法?
對(duì)象聲明 —— 一個(gè)關(guān)鍵字實(shí)現(xiàn)單例 ?
伴生對(duì)象 —— static 的代替者 ?
對(duì)象表達(dá)式 —— Kotlin 的匿名內(nèi)部類 ?
這到底是哪種用法 ?
正文
object 的三種用法
Kotlin 的 object
關(guān)鍵字有三種用法:
對(duì)象聲明 ,一般用來實(shí)現(xiàn)單例伴生對(duì)象 ,類似 Java 的 static 關(guān)鍵字,也可以用于工廠方法模式對(duì)象表達(dá)式 ,一般用來代替 Java 的匿名內(nèi)部類
下面就逐個(gè)來看看這三種用法的本質(zhì)。
對(duì)象聲明
object
的語(yǔ)義是這樣的: 定義一個(gè)類并創(chuàng)建一個(gè)實(shí)例 。不管是對(duì)象聲明,還是下面會(huì)說到的另外兩種用法,都是遵循這一語(yǔ)義的。
作為對(duì)象聲明,它可以直接用來實(shí)現(xiàn)單例模式:
object Singleton{
fun xxx(){}
}
話不多說,直接 Decompile 看 Java 代碼:
public final class Singleton {
public static final Singleton INSTANCE;
public final void xxx() {
}
private Singleton() {
}
static {
Singleton var0 = new Singleton();
INSTANCE = var0;
}
}
從 Java 代碼中可以看出來,顯然這是一個(gè)單例模式。
私有構(gòu)造函數(shù) 通過靜態(tài)字段對(duì)外提供實(shí)例 靜態(tài)代碼塊中直接初始化,線程安全 。
這里插播一個(gè)問題,static 代碼塊在何時(shí)執(zhí)行?
首先類加載階段可以分為加載 、驗(yàn)證 、準(zhǔn)備 、解析 、初始化 、使用 、卸載 七個(gè)步驟 。static 代碼塊就是在 初始化 階段執(zhí)行的。那么,哪些場(chǎng)景會(huì)觸發(fā)類的初始化呢?有如下幾種場(chǎng)景:
通過 new
實(shí)例化對(duì)象 讀寫一個(gè)類的靜態(tài)字段 調(diào)用一個(gè)類的靜態(tài)方法 對(duì)類進(jìn)行反射調(diào)用
按照上面反編譯出來的 Java 代碼,獲得單例對(duì)象的方法是 Singleton.INSTANCE
,即調(diào)用 Singleon
類的靜態(tài)字段 INSTANCE
,就會(huì)觸發(fā)類的初始化階段,也就觸發(fā)了 static 代碼塊的執(zhí)行,從而完成了單例對(duì)象的實(shí)例化。同時(shí),由于類加載過程天生線程安全,所以 Kotlin 的 object 單例活脫脫的就是一個(gè)線程安全的懶漢式單例(訪問時(shí)初始化)。
此外,object 聲明的單例類和普通類一樣,可以實(shí)現(xiàn)接口,繼承類,也可以包含屬性,方法。但是它不能由開發(fā)者手動(dòng)聲明構(gòu)造函數(shù),從反編譯出來的 Java 代碼可以看到,它只有一個(gè) private
構(gòu)造函數(shù)。
所以,這對(duì)實(shí)際的業(yè)務(wù)場(chǎng)景是有一定限制的。對(duì)于需要攜帶參數(shù)的單例類,object 就有點(diǎn)力不從心了。當(dāng)然也不難解決,模仿 Java 的寫法就行了,這里以 DCL 模式為例。
class Singleton private constructor(private val param: Int) {
companion object {
@Volatile
private var instance: Singleton? = null
fun getInstance(property: Int) =
instance ?: synchronized(this) {
instance ?: Singleton(property).also { instance = it }
}
}
}
說到這,你應(yīng)該了解了 object
實(shí)現(xiàn)單例模式的本質(zhì)。下面來看看 伴生對(duì)象 。
伴生對(duì)象
你可以回想一下,你在 Kotlin 中使用過 static
關(guān)鍵字嗎?答案肯定是沒有。通常我們可以在頂層文件中直接定義常量和頂層函數(shù),但有的時(shí)候我們的確需要在類中定義靜態(tài)常量或函數(shù),這樣顯得更加直觀。這就是 伴生對(duì)象 的應(yīng)用場(chǎng)景。
伴生對(duì)象 ,顧名思義,就是伴隨著類而存在的對(duì)象,在類加載的時(shí)候初始化。
class User(val male: Int){
companion object {
val MALE = 0
fun isMale(male:Int) = male == MALE
}
}
這樣就可以像調(diào)用 static 一樣調(diào)用伴生對(duì)象中的屬性和函數(shù),而無(wú)需創(chuàng)造類實(shí)例。
User.MALE
User.isMale(1)
還是直接看 Java 代碼。
public final class User {
private final int male;
private static final int MALE = 0;
public static final User.Companion Companion = new User.Companion((DefaultConstructorMarker)null);
public final int getMale() {
return this.male;
}
public User(int male) {
this.male = male;
}
public static final class Companion {
public final int getMALE() {
return User.MALE;public static final User.Companion Companion = new User.Companion((DefaultConstructorMarker)null);
}
public final boolean isMale(int male) {
return male == ((User.Companion)this).getMALE();
}
private Companion() {
}
// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
編譯器為我們生成了一個(gè)叫做 Companion
的靜態(tài)內(nèi)部類,注意它的 getMale()
和 isMale()
方法并不是靜態(tài)方法,所以實(shí)際去訪問的時(shí)候還是需要一個(gè) Companion
實(shí)例的。這里實(shí)例就是 User
類中定義的靜態(tài)成員變量 Companion
:
public static final User.Companion Companion = new User.Companion((DefaultConstructorMarker)null);
看到靜態(tài)字段,又該想到在類加載的時(shí)候初始化的了。那么,哪個(gè)操作觸發(fā)了類加載呢?我們來反編譯一下 User.MALE
的 Java 代碼。
User.Companion.getMALE();
所以也是訪問時(shí)時(shí)會(huì)初始化伴生對(duì)象。再回想一下前面說過的,
object
其實(shí)我們可以把它理解成 定義一個(gè)類并創(chuàng)建一個(gè)實(shí)例 。
伴生對(duì)象仍舊符合這一語(yǔ)義。
在 Java 中如何調(diào)用伴生對(duì)象呢?User.Companion.isMale(1)
即可。另外,我們可以給伴生對(duì)象命名,如下所示:
companion object X { ... }
那么,編譯器生成的類就不是 Companion
了,而是 X
。在 Java 中就可以用 User.X.isMale(1)
了。
了解了伴生對(duì)象的本質(zhì)之后,再來說兩個(gè)它的其他用法。
創(chuàng)建靜態(tài)工廠方法
interface Car {
val brand: String
companion object {
operator fun invoke(type: CarType): Car {
return when (type) {
CarType.AUDI -> Audi()
CarType.BMW -> BMW()
}
}
}
}
這里重載了 invoke()
方法,調(diào)用時(shí)直接 Car(CarType.BMW)
即可。你可以試著用 Java 代碼實(shí)現(xiàn)上面的邏輯,對(duì)比一下。
伴生對(duì)象擴(kuò)展方法
伴生對(duì)象也是支持?jǐn)U展方法的。還是以上面的 Car
為例,定義一個(gè)根據(jù)汽車品牌獲取汽車類型的擴(kuò)展方法。
fun Car.Companion.getCarType(brand:String) :CarType { ...... }
雖然是在 Car.Companion
上定義的擴(kuò)展函數(shù),但實(shí)際上相當(dāng)于給 Car
增加了一個(gè)靜態(tài)方法,使用方式如下:
Car.getCarType("BMW")
對(duì)象表達(dá)式
對(duì)象表達(dá)式最經(jīng)典的用法就是用來 代替 Java 的匿名內(nèi)部類 。例如常見的點(diǎn)擊事件:
xxx.setOnClickListener(object : View.OnClickListener{
override fun onClick(v: View) {
}
})
這和 Java 的匿名內(nèi)部類是等價(jià)的。只不過像上面的單方法接口,我們很少用 object
寫,而是用 lambda
代替,顯得更加簡(jiǎn)潔。
xxx.setOnClickListener { view -> ...... }
當(dāng)匿名對(duì)象需要重寫多個(gè)方法時(shí),就只能選擇對(duì)象表達(dá)式了。
和 Java 的匿名內(nèi)部類一樣,對(duì)象聲明中也可以訪問外部變量。
對(duì)象表達(dá)式應(yīng)該是 object
最樸實(shí)無(wú)華的使用方式了。
最后
看到這里,你應(yīng)該已經(jīng)完全掌握了 object
關(guān)鍵字的本質(zhì)。那么,我也要來考考你,仔細(xì)看下面的代碼:
class MainActivity : AppCompatActivity() {
val a = 1
object click : View.OnClickListener {
override fun onClick(v: View) {
val b = a + 1
}
}
}
上面的代碼可以正確編譯嗎?為什么? 這里 object
的用法屬于哪一種?史上快重學(xué) Kotlin —— object,史上最 “快” 單例 ?