Kotlin 對(duì)象表達(dá)式和伴生對(duì)象
本篇文章將是 Kotlin 面向?qū)ο笙盗械淖詈笠黄恼?,這篇文章將會(huì)介紹幾個(gè)特殊的對(duì)象語法,這是 Kotlin 語法中獨(dú)有的。比如對(duì)象表達(dá)式 (object),天生的單例對(duì)象它會(huì)使寫一個(gè)單例模式變得特別簡單,而不是像 Java 那樣聲明一些語法模板。此外伴生對(duì)象 (companion object) 它將替代 Java 中的 static 靜態(tài)成員。
1. 為什么需要對(duì)象表達(dá)式
1.1 對(duì)象表達(dá)式天生單例,會(huì)使得單例模式更簡單
相信很多小伙伴都手寫過 Java 中的單例模式,我們熟知單例模式必須滿足幾個(gè)條件:
- 構(gòu)造器私有化,private 修飾,主要為了防止外部私自創(chuàng)建該單例類的對(duì)象實(shí)例
- 提供一個(gè)該實(shí)例對(duì)象全局訪問點(diǎn),在 Java 中一般是以公有的靜態(tài)方法或者枚舉返回單例類對(duì)象
- 在多線程環(huán)境下保證單例類有且只有一個(gè)對(duì)象實(shí)例,以及在多線程環(huán)境下獲取單例類對(duì)象實(shí)例需要保證線程安全。
- 在反序列化時(shí)保證單例類有且只有一個(gè)對(duì)象實(shí)例。
其實(shí)在 Java 中實(shí)現(xiàn)一個(gè)單例模式,上述條件需要寫一些模板的代碼,比如下面代碼:
public class Singleton implements Serializable {
private Singleton() {//構(gòu)造器私有化
}
private static final Singleton mInstance = new Singleton();
public static Singleton getInstance() {//提供公有獲取單例對(duì)象的函數(shù)
return mInstance;
}
//防止單例對(duì)象在反序列化時(shí)重新生成對(duì)象
private Object readResolve() throws ObjectStreamException {
return mInstance;
}
}
//外部調(diào)用
public class TestMain {
public static void main(String[] args) {
Singleton.getInstance().doSomething();
}
}
然而上述近 15 行 Java 的代碼,在 Kotlin 中使用單例,只需要簡單聲明一個(gè) object 表達(dá)式,然后在表達(dá)式內(nèi)部定義單例方法即可:
object KSingleton : Serializable {//實(shí)現(xiàn)Serializable序列化接口,通過私有、被實(shí)例化的readResolve方法控制反序列化
fun doSomething() {
println("do some thing")
}
private fun readResolve(): Any {//防止單例對(duì)象在反序列化時(shí)重新生成對(duì)象
return KSingleton//由于反序列化時(shí)會(huì)調(diào)用readResolve這個(gè)鉤子方法,只需要把當(dāng)前的KSingleton對(duì)象返回而不是去創(chuàng)建一個(gè)新的對(duì)象
}
}
為什么一行簡單的 object 對(duì)象表達(dá)式就能實(shí)現(xiàn)單例,實(shí)際上這就是編譯器魔法或者說是語法糖,本質(zhì)上單例還是單例規(guī)則,這一點(diǎn)是無法改變的,而是編譯器在編譯 object 期間生成額外的單例代碼,所以要想揭開這層語法糖衣還得將 Kotlin 代碼反編譯成 Java 代碼。
public final class KSingleton implements Serializable {
public static final KSingleton INSTANCE;
public final void doSomething() {
String var1 = "do some thing";
System.out.println(var1);
}
private final Object readResolve() {
return INSTANCE;//可以看到readResolve方法直接返回了INSTANCE而不是創(chuàng)建新的實(shí)例
}
static {//靜態(tài)代碼塊初始化KSingleton實(shí)例,不管有沒有使用,只要KSingleton被加載了,
//靜態(tài)代碼塊就會(huì)被調(diào)用,KSingleton實(shí)例就會(huì)被創(chuàng)建,并賦值給INSTANCE
KSingleton var0 = new KSingleton();
INSTANCE = var0;
}
}
可能會(huì)有人疑惑:沒有看到構(gòu)造器私有化,實(shí)際上這一點(diǎn)已經(jīng)在編譯器層面做了限制,不管你是在 Java 還是 Kotlin 中都無法私自去創(chuàng)建新的 object 單例對(duì)象。
1.2 替代 Java 中的匿名內(nèi)部類
我們都知道在 Java 中有匿名內(nèi)部類,一般情況直接通過 new 匿名內(nèi)部類對(duì)象,然后重寫內(nèi)部抽象方法。但是在 Kotlin 使用 object 對(duì)象表達(dá)式來替代了匿名內(nèi)部類,一般匿名內(nèi)部類用在接口回調(diào)比較多。比如 Java 實(shí)現(xiàn)匿名內(nèi)部類:
public interface OnClickListener {
void onClick(View view);
}
mButton.setOnClickListener(new OnClickListener() {//Java創(chuàng)建匿名內(nèi)部類對(duì)象
@Override
public void onClick(View view) {
//do logic
}
});
然而在 Kotlin 并不是直接創(chuàng)建一個(gè)匿名接口對(duì)象,而是借助 object 表達(dá)式來聲明的。
interface OnClickListener {
fun onClick()
}
mButton.setOnClickListener(object: OnClickListener{//Kotlin創(chuàng)建object對(duì)象表達(dá)式
override fun onClick() {
//do logic
}
})
2. 如何使用對(duì)象表達(dá)式
使用對(duì)象表達(dá)式很簡單,只需要像聲明類一樣聲明即可,只不過把 class 關(guān)鍵字換成了 object. 聲明格式: object + 對(duì)象名 + : + 要實(shí)現(xiàn) / 繼承的接口或抽象類 (用做單例模式場景) 和 object + : + 要實(shí)現(xiàn) / 繼承的接口或抽象類 (用做匿名內(nèi)部類場景):
//1、用做單例模式形式
object KSingleton : Serializable {//object關(guān)鍵字 + 對(duì)象名(KSingleton) + : + 要實(shí)現(xiàn)的接口(Serializable)
fun doSomething() {
println("do some thing")
}
private fun readResolve(): Any {
return KSingleton
}
}
//2、用做匿名內(nèi)部類形式
mButton.setOnClickListener(object: OnClickListener{//object關(guān)鍵字 + : + 要實(shí)現(xiàn)的接口(OnClickListener)
override fun onClick() {
//do logic
}
})
3. 對(duì)象表達(dá)式使用場景
在 Kotlin 中 object 對(duì)象表達(dá)式使用場景主要就是單例模式和替代匿名內(nèi)部類場景。
3.1 object 用于單例模式場景
單例場景很簡單,如果有需要使用單例模式,只要聲明一個(gè) object 對(duì)象表達(dá)式即可:
object KSingleton : Serializable {//實(shí)現(xiàn)Serializable序列化接口,通過私有、被實(shí)例化的readResolve方法控制反序列化
fun doSomething() {
println("do some thing")
}
private fun readResolve(): Any {//防止單例對(duì)象在反序列化時(shí)重新生成對(duì)象
return KSingleton//由于反序列化時(shí)會(huì)調(diào)用readResolve這個(gè)鉤子方法,只需要把當(dāng)前的KSingleton對(duì)象返回而不是去創(chuàng)建一個(gè)新的對(duì)象
}
}
//在Kotlin中使用KSingleton
fun main(args: Array<String>) {
KSingleton.doSomething()//像調(diào)用靜態(tài)方法一樣,調(diào)用單例類中的方法
}
//在Java中使用KSingleton
public class TestMain {
public static void main(String[] args) {
KSingleton.INSTANCE.doSomething();//通過拿到KSingleton的公有單例類靜態(tài)實(shí)例INSTANCE, 再通過INSTANCE調(diào)用單例類中的方法
}
}
3.2 object 用于匿名內(nèi)部類場景
object 使用匿名內(nèi)部場景在開發(fā)中還是比較多的,對(duì)于需要寫一些接口回調(diào)方法時(shí),一般都離不開 object 對(duì)象表達(dá)式。
interface OnClickListener {
fun onClick()
}
mButton.setOnClickListener(object: OnClickListener{//Kotlin創(chuàng)建object對(duì)象表達(dá)式
override fun onClick() {
//do logic
}
})
3.3 object 匿名內(nèi)部類場景和 lambda 表達(dá)式場景如何選擇
其實(shí)我們知道在 Kotlin 中對(duì)于匿名內(nèi)部類場景,除了可以使用 object 對(duì)象表達(dá)式場景還可以使用 lambda 表達(dá)式,但是需要注意的是能使用 lambda 表達(dá)式替代匿名內(nèi)部場景必須是匿名內(nèi)部類使用的類接口中只能有一個(gè)抽象方法,超過一個(gè)都不能使用 lambda 表達(dá)式來替代。所以對(duì)于 object 表達(dá)式和 lambda 表達(dá)式替換匿名內(nèi)部類場景就一目了然了。
interface OnClickListener {
fun onClick()
}
mButton.setOnClickListener{//因?yàn)镺nClickListener中只有一個(gè)抽象方法onClick,所以可以直接使用lambda表達(dá)式的簡寫形式
//do logic
}
interface OnLongClickListener {
fun onLongClick()
fun onClickLog()
}
mButton.setOnLongClickListener(object : OnLongClickListener {//因?yàn)镺nLongClickListener中有兩個(gè)抽象方法,所以只能使用object表達(dá)式這種形式
override fun onLongClick() {
}
override fun onClickLog() {
}
})
4. 伴生對(duì)象
在 Kotlin 中其實(shí)已經(jīng)看不到任何的 static
靜態(tài)關(guān)鍵字的字眼,我們都知道在 Java 中 static 是一個(gè)非常重要的特性,它可以用來修飾類、方法或?qū)傩?。然而,static 修飾的內(nèi)容是屬于類級(jí)別的,而不是具體的對(duì)象級(jí)別的,但是很奇怪的是在定義的時(shí)候卻與普通類成員變量和方法混合在一起。站在 Kotlin 視角來看就覺得代碼結(jié)構(gòu)很混亂,所以 Kotlin 希望盡管屬于類級(jí)別的,定義在類的內(nèi)部也希望把所有類似靜態(tài)方法、屬性都定義一個(gè)叫做 companion object 局部作用域內(nèi),這樣一看就知道在 companion object 中是靜態(tài)的。一起來看下對(duì)比例子:
package com.imooc.test;
public class Teacher {
private String name;
private String sex;
private int age;
public Teacher(String name, String sex, int age) {
this.name = name;
this.sex = sex;
this.age = age;
}
//可以看到在Java中static方法和屬性都和普通類Teacher中普通成員方法屬性混在一起的
static final String MALE = "male";
static final String FEMALE = "FEMALE";
public static boolean isWomen(Teacher teacher) {
return teacher.sex.equals(FEMALE);
}
}
Kotlin 希望能找到一種方式能夠?qū)刹糠执a分開,但是又不失語義,所以 Kotlin 在 object 對(duì)象表達(dá)式基礎(chǔ)上,引入了伴生對(duì)象的概念,伴生對(duì)象故名思義就是伴隨某個(gè)類的對(duì)象,它屬于這個(gè)類所有,因此伴生對(duì)象和 Java 中 static 修飾效果性質(zhì)一樣,全局只有一個(gè)單例。它聲明在類的內(nèi)部,在類被裝載的時(shí)候初始化。
package com.imooc.test
class KTeacher(private var name: String, private var sex: String, private var age: Int) {
companion object {//在KTeacher類內(nèi)部,提出companion object作用域?qū)⑺衧tatic相關(guān)的成員屬性和方法放在一起,這樣就可以和普通成員屬性和方法分隔開了
private const val MALE = "male"
private const val FEMALE = "female"
fun isWoman(teacher: KTeacher): Boolean {
return teacher.sex == FEMALE
}
}
}
54. 總結(jié)
到這里有關(guān) Kotlin 中面向?qū)ο笙嚓P(guān)系列知識(shí)就介紹完畢了,總的體會(huì)下來會(huì)發(fā)現(xiàn) Kotlin 的面向?qū)ο笥泻芏嗟胤竭€是和 Java 保持一樣,但是也引入很多自己特有的語法,然而在這些語法的背后依然還是回歸到 Java 上了。但是不得不說這語法糖是真甜,寫代碼的效率將是大大提高,這也是 Kotlin 這門語言初衷,減少不必要模板代碼,讓開發(fā)者更加專注于自己業(yè)務(wù)邏輯。下篇文章將進(jìn)入 Kotlin 比較復(fù)雜且不易理解的泛型系列,會(huì)由淺入深地帶你認(rèn)識(shí) Kotlin 泛型的使用場景以及它背后的原理。