Kotlin Vocabulary | 唯一的 "對象"

在 Java 語言中,static 關鍵字主要用於表明方法和屬性是屬於某個對象,而不是屬於對象的實例。static 關鍵字也用於創建 Singleton (單例),單例模式是非常常見的設計模式,它可以幫您創建某個對象的唯一實例,並且其它對象也可以訪問和分享該實例。

Kotlin 可以更加優雅地實現這種設計模式。您只需使用一個關鍵字: object,就可以實現單例。接下來的內容會告訴大家在 Java 和 Kotlin 中實現單例的區別,以及在 Kotlin 中如何在不使用 static 關鍵字的情況下實現單例,(其實就是通過 object 關鍵字實現的),然後爲大家詳解使用 object 時底層的實現機制。

首先,我們先聊聊這個應用場景的背景 —— 爲什麼我們需要一個單例呢?

什麼是單例?

單例是一種設計模式,它保證一個類只有唯一一個實例,並且提供全局可訪問該對象的接口。單例非常適合那些需要在應用的不同地方共享的對象,以及初始化實例非常消耗資源的場景下使用。

Java 中的單例

要保證一個類只有一個實例,您需要控制對象的創建方式。要使類有且僅有一個實例,需要將構造方法定義爲私有的 (private),並且創建一個公共可訪問的靜態對象引用。與此同時,您一般不會在啓動的時候創建單例,因爲使用單例的對象在創建的時候非常耗費資源。要實現這個目的,需要提供一個靜態方法,方法裏會檢查是否已經創建該對象。這個靜態方法必須返回之前創建的實例,或者調用構造函數然後返回實例。

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

public class Singleton{
    private static Singleton INSTANCE;
    private Singleton(){}
    public static Singleton getInstance(){
        if (INSTANCE == null){
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
    private int count = 0;
    public int count(){ return count++; }
}

上面的代碼看上去沒什麼,但是其實裏面有個大問題: 這段代碼無法保證線程安全。某一時刻當一個線程剛剛運行完 if 語句的時候有可能被掛起,而與此同時另外一個線程調用該方法並且創建單例。而之前被掛起的線程會繼續運行,並創建另外一個實例。

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

public class Singleton{
    private static Singleton INSTANCE = new Singleton();
    private Singleton(){}
    public static Singleton getInstance(){
        if (INSTANCE == null) {                // 一次檢查
            synchronized (Singleton.class) {
                if (INSTANCE == null) {        // 二次檢查
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
    private int count = 0;
    public int count(){ return count++; }
}

要解決線程同步的問題,您可以使用二次檢查鎖定。在二次檢查鎖定中,如果實例爲空,則會通過 synchronized 關鍵字創建同步鎖並,且對實例進行二次檢查保證當前實例仍爲空。如果此時實例仍爲空,那麼會創建單例。然而這還不夠,單例對象還需要使用 volatile 關鍵字。volatile 關鍵字告訴編譯器該變量可能會被併發運行的線程異步修改。

上述內容就會導致大量的模板代碼,每次當您創建單例時就需要重複它們。對於這麼一個簡單的任務卻使用瞭如此繁雜的代碼,所以 Java 中創建單例時通常會使用 枚舉

Kotlin 中的單例

那麼我們再來看看 Kotlin。Kotlin 中並沒有靜態方法或者靜態字段,那麼我們如何在 Kotlin 中創建單例呢?

實際上,可以通過 Android Studio/IntelliJ 來幫助我們理解這一問題。當您將 Java 的單例代碼轉換爲 Kotlin 代碼時,所有的靜態屬性和方法就會被移動到 companion object 中。

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

class Singleton private constructor() {
    private var count = 0
    fun count(): Int {
        return count++
    }

    companion object {
        private var INSTANCE: Singleton? =
            Singleton()//二次檢查

        // Single Checked
        val instance: Singleton?
            get() {
                if (INSTANCE == null) { //首次檢查
                    synchronized(Singleton::class.java) {
                        if (INSTANCE == null) { //二次檢查
                            INSTANCE =
                                Singleton()
                        }
                    }
                }
                return INSTANCE
            }
    }
}

經過轉換的代碼已經夠實現我們想要的效果,但其實可以讓代碼更簡潔。可以從 object 代碼中去掉構造函數和 companion 關鍵字。文章後面會告訴大家 object 和 companion objects 之間的區別。

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

object Singleton {
    private var count: Int = 0

    fun count() {
        count++
    }
}

當您想使用 count() 方法的時候,您可以通過 Singleton 對象調用該方法。在 Kotlin 中,object 是一種特殊的類,它只有一個實例。如果您創建類的時候使用的是 object 關鍵字而不是 class,Kotlin 編譯器會將構造方法設置爲私有的,並且爲 object 類創建一個靜態引用,同時在一個靜態代碼塊裏初始化該引用。

當靜態字段第一次被訪問的時候會調用靜態代碼塊一次。即使沒有 synchronized 關鍵字,JVM 處理靜態代碼塊和處理同步代碼塊的方式相類似。當 Singleton 類進行初始化的時候,JVM 會從同步代碼塊中獲得一個鎖,如此一來,其它線程就無法訪問它。當同步鎖被釋放的時候,就已經創建了 Singleton 實例,並且該靜態代碼塊也不會再被運行。這樣就保證了有且僅有一個 Singleton 實例,這就滿足了單例的要求。這樣一來,object 即保證了線程安全,也實現了首次訪問的延遲創建。

我們來看一下反編譯的 Kotlin 字節碼,深入瞭解一下底層是如何實現的。

如果想查看一個 Kotlin 類的字節碼,依次選擇 Tools > Kotlin > Show Kotlin Bytecode。Kotlin 字節碼顯示以後,點擊 Decompile 來查看反編譯的 Java 代碼。

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

public final class Singleton {
   private static int count;
   public static final Singleton INSTANCE;
   public final int getCount() {return count;}
   public final void setCount(int var1) {count = var1;}
   public final int count() {
      int var1 = count++;
      return var1;
   }
   private Singleton() {}
   static {
      Singleton var0 = new Singleton();
      INSTANCE = var0;
   }
}

然而 object 也有一定的限制。object 聲明裏不能包含構造函數,也就是說無法傳參給它。即使它支持傳參,由於靜態代碼塊無法訪問構造方法中的非靜態參數,所以傳入的參數也無法使用。

⚠️ 和其它靜態方法一樣,靜態的初始化代碼塊只能訪問一個類的靜態屬性。靜態的代碼塊的調用要早於構造方法,所以靜態代碼塊無法訪問對象的屬性或者傳遞給構造函數的參數。

companion object

companion object 和 object 相類似。companion object 常常在類中聲明,並且它們的屬性可以通過宿主類名進行訪問。companion object 不需要定義名稱。如果定義了 companion object 的名稱,也可以通過名稱來訪問它的類成員。

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

class SomeClass {
    //…
    companion object {
        private var count: Int = 0
        fun count() {
            count++
        }
    }
}
class AnotherClass {
    //…
    companion object Counter {
        private var count: Int = 0
        fun count() {
            count++
        }
    }
}
//不定義名稱的場景
SomeClass.count()
//定義名稱的場景
AnotherClass.Counter.count()

舉個例子,這裏我們有兩個相似的類定義,分別是帶名稱和不帶名稱的 companion object。可以通過 SomeClass 來調用 count() 方法,就像 SomeClass 的靜態成員一樣;或者也可以通過使用 Counter 來調用 count() 方法,就像 AnotherClass 的靜態成員一樣。

反編譯 companion object 會得到一個帶有私有構造函數的內聯類。宿主類會通過一個合成構造方法來初始化一個內部類,這個內部類只有宿主類才能夠訪問。宿主類會保持一個 companion object 的公共引用,可用於其它類訪問。

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

public final class AnotherClass {
    private static int count;
    public static final AnotherClass.Counter Counter = new AnotherClass.Counter((DefaultConstructorMarker)null);

    public static final class Counter {
        public final void count() {
            AnotherClass.count = AnotherClass.count + 1;
        }
        private Counter() { }
        // $FF: synthetic method
        public Counter(DefaultConstructorMarker $constructor_marker) {
            this();
        }
    }
    
    public static final class Companion {
        public final void count() {
            AnotherClass.count = AnotherClass.count + 1;
        }
        private Companion() {}
    }
}

Object 表達式

到目前爲止,我們已經在聲明對象的時候使用了 object 關鍵字,不過它也可以用於 object 表達式。當作爲表達式使用時,object 關鍵字可以幫助您創建匿名對象和匿名內部類。

比如您需要一個臨時對象來保持一些數據值時,可以立即聲明對象並使用所需的數值進行初始化,之後再訪問它們。

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

val tempValues = object : {
    var value = 2
    var anotherValue = 3
    var someOtherValue = 4
}

tempValues.value += tempValues.anotherValue

在所生成的代碼中,這個操作會被轉換爲一個匿名的 Java 類,並且該對象會被標記爲 <undefinedtype> 來保存匿名對象及其 getter 和 setter。

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

<undefinedtype> tempValues = new Object() {
    private int value = 2;
    private int anotherValue = 3;
    private int someOtherValue = 4;

    // x,y,z 的 getter 和 setter
    //...
};

object 關鍵字無需使用模板代碼就可以創建匿名類。您可以使用 object 表達式,而 Kotlin 編譯器則會生成包裹類的聲明來創建一個匿名類。

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

//Kotlin 代碼
val t1 = Thread(object : Runnable {    
    override fun run() {
         // 邏輯代碼
    }
})
t1.start()

//反編譯的 Java 代碼
Thread t1 = new Thread((Runnable)(new Runnable() {
     public void run() {
     
     }
}));
t1.start();

object 關鍵字會幫助您使用更少的代碼創建線程安全的單例、匿名對象和匿名類。通過 object 和 companion object, Kotlin 會生成全部所需的代碼來實現類似 static 關鍵字的功能。此外,您還可以避免模板代碼的困擾,僅通過使用 object 表達式來創建匿名對象和匿名類。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章