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