Kotlin Vocabulary | 惟一的 "對象"

在 Java 語言中,static 關鍵字主要用於代表方法和屬性是屬於某個對象,而不是屬於對象的實例。static 關鍵字也用於建立 Singleton (單例),單例模式是很是常見的設計模式,它能夠幫您建立某個對象的惟一實例,而且其它對象也能夠訪問和分享該實例。java

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 語句的時候有可能被掛起,而與此同時另一個線程調用該方法而且建立單例。而以前被掛起的線程會繼續運行,並建立另一個實例。ide

<!-- 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 中建立單例時一般會使用 枚舉this

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 表達式來建立匿名對象和匿名類。

相關文章
相關標籤/搜索