Kotlin下的5種單例模式

AndyandJennifer發表於2019-03-03

Kotlin.jpg

前言

最近在學習Kotlin這門語言,在專案開發中,運用到了單例模式。因為其表達方式與Java是不同的。所以對不同單例模式的實現進行了分別探討。主要單例模式實現如下:

  • 餓漢式
  • 懶漢式
  • 執行緒安全的懶漢式
  • 雙重校驗鎖式
  • 靜態內部類式

PS:該篇文章不討論單例模式的運用場景與各種模式下的單例模式的優缺點。只討論在Java下不同單例模式下的對應Kotlin實現。

一、餓漢式實現

//Java實現
public class SingletonDemo {
    private static SingletonDemo instance=new SingletonDemo();
    private SingletonDemo(){

    }
    public static SingletonDemo getInstance(){
        return instance;
    }
}
//Kotlin實現
object SingletonDemo
複製程式碼

這裡很多小夥伴,就吃了一驚。我靠一個object 關鍵字就完成相同的功能?一行程式碼?

Kotlin的物件宣告

學習了Kotlin的小夥伴肯定知道,在Kotlin中類沒有靜態方法。如果你需要寫一個可以無需用一個類的例項來呼叫,但需要訪問類內部的函式(例如,工廠方法,單例等),你可以把該類宣告為一個物件。該物件與其他語言的靜態成員是類似的。如果你想了解Kotlin物件宣告的更多內容。請點選- - - 傳送門

到這裡,如果還是有很多小夥伴不是很相信一行程式碼就能解決這個功能,我們可以通過一下方式檢視Kotlin的位元組碼。

檢視Kotlin對應位元組碼

檢視Kotlin位元組碼.png

我們進入我們的Android Studio(我的Android Studio 3.0,如果你的編譯器版本過低,請自動升級) 選擇Tools工具欄,選擇"Kotlin",選擇“Show Kotlin Bytecode"

選擇過後就會進入到下方介面:

檢視Kotlin位元組碼.png

點選"Decompile" 根據位元組碼得到以下程式碼

public final class SingletonDemo {
   public static final SingletonDemo INSTANCE;
   private SingletonDemo(){}
   static {
      SingletonDemo var0 = new SingletonDemo();
      INSTANCE = var0;
   }
}
複製程式碼

通過以上程式碼,我們瞭解事實就是這個樣子的,使用Kotlin"object"進行物件宣告與我們的餓漢式單例的程式碼是相同的。

二、懶漢式

//Java實現
public class SingletonDemo {
    private static SingletonDemo instance;
    private SingletonDemo(){}
    public static SingletonDemo getInstance(){
        if(instance==null){
            instance=new SingletonDemo();
        }
        return instance;
    }
}
//Kotlin實現
class SingletonDemo private constructor() {
    companion object {
        private var instance: SingletonDemo? = null
            get() {
                if (field == null) {
                    field = SingletonDemo()
                }
                return field
            }
        fun get(): SingletonDemo{
        //細心的小夥伴肯定發現了,這裡不用getInstance作為為方法名,是因為在伴生物件宣告時,內部已有getInstance方法,所以只能取其他名字
         return instance!!
        }
    }
}
複製程式碼

上述程式碼中,我們可以發現在Kotlin實現中,我們讓其主建構函式私有化並自定義了其屬性訪問器,其餘內容大同小異。

  • 如果有小夥伴不清楚Kotlin建構函式的使用方式。請點選 - - - 建構函式
  • 不清楚Kotlin的屬性與訪問器,請點選 - - -屬性和欄位

三、執行緒安全的懶漢式

//Java實現
public class SingletonDemo {
    private static SingletonDemo instance;
    private SingletonDemo(){}
    public static synchronized SingletonDemo getInstance(){//使用同步鎖
        if(instance==null){
            instance=new SingletonDemo();
        }
        return instance;
    }
}
//Kotlin實現
class SingletonDemo private constructor() {
    companion object {
        private var instance: SingletonDemo? = null
            get() {
                if (field == null) {
                    field = SingletonDemo()
                }
                return field
            }
        @Synchronized
        fun get(): SingletonDemo{
            return instance!!
        }
    }

}
複製程式碼

大家都知道在使用懶漢式會出現執行緒安全的問題,需要使用使用同步鎖,在Kotlin中,如果你需要將方法宣告為同步,需要新增**@Synchronized**註解。

四、雙重校驗鎖式(Double Check)

//Java實現
public class SingletonDemo {
    private volatile static SingletonDemo instance;
    private SingletonDemo(){} 
    public static SingletonDemo getInstance(){
        if(instance==null){
            synchronized (SingletonDemo.class){
                if(instance==null){
                    instance=new SingletonDemo();
                }
            }
        }
        return instance;
    }
}
//kotlin實現
class SingletonDemo private constructor() {
    companion object {
        val instance: SingletonDemo by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
        SingletonDemo() }
    }
}
複製程式碼

哇!小夥伴們驚喜不,感不感動啊。我們居然幾行程式碼就實現了多行的Java程式碼。其中我們運用到了Kotlin的延遲屬性 Lazy

Lazy是接受一個 lambda 並返回一個 Lazy 例項的函式,返回的例項可以作為實現延遲屬性的委託: 第一次呼叫 get() 會執行已傳遞給 lazy() 的 lambda 表示式並記錄結果, 後續呼叫 get() 只是返回記錄的結果。

這裡還有有兩個額外的知識點。

  • 高階函式,高階函式是將函式用作引數或返回值的函式(我很糾結我到底講不講,哎)。大家還是看這個 ---高階函式
  • 委託屬性

如果你瞭解以上知識點,我們直接來看Lazy的內部實現。

Lazy內部實現
public fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
        when (mode) {
            LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
            LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
            LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
        }
複製程式碼

觀察上述程式碼,因為我們傳入的mode = LazyThreadSafetyMode.SYNCHRONIZED, 那麼會直接走 SynchronizedLazyImpl,我們繼續觀察SynchronizedLazyImpl。

Lazy介面

SynchronizedLazyImpl實現了Lazy介面,Lazy具體介面如下:

public interface Lazy<out T> {
     //當前例項化物件,一旦例項化後,該物件不會再改變
    public val value: T
    //返回true表示,已經延遲例項化過了,false 表示,沒有被例項化,
    //一旦方法返回true,該方法會一直返回true,且不會再繼續例項化
    public fun isInitialized(): Boolean
}
複製程式碼

繼續檢視SynchronizedLazyImpl,具體實現如下:

SynchronizedLazyImpl內部實現
private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE
    // final field is required to enable safe publication of constructed instance
    private val lock = lock ?: this

    override val value: T
        get() {
            val _v1 = _value
            //判斷是否已經初始化過,如果初始化過直接返回,不在呼叫高階函式內部邏輯
            if (_v1 !== UNINITIALIZED_VALUE) {
                @Suppress("UNCHECKED_CAST")
                return _v1 as T
            }

            return synchronized(lock) {
                val _v2 = _value
                if (_v2 !== UNINITIALIZED_VALUE) {
                    @Suppress("UNCHECKED_CAST") (_v2 as T)
                }
                else {
                    val typedValue = initializer!!()//呼叫高階函式獲取其返回值
                    _value = typedValue   //將返回值賦值給_value,用於下次判斷時,直接返回高階函式的返回值
                    initializer = null
                    typedValue  
                }
            }
        }
		//省略部分程式碼
}
複製程式碼

通過上述程式碼,我們發現 SynchronizedLazyImpl 覆蓋了Lazy介面的value屬性,並且重新了其屬性訪問器。其具體邏輯與Java的雙重檢驗是類似的。

到裡這裡其實大家還是肯定有疑問,我這裡只是例項化了SynchronizedLazyImpl物件,並沒有進行值的獲取,它是怎麼拿到高階函式的返回值呢?。這裡又涉及到了委託屬性

委託屬性語法是: val/var <屬性名>: <型別> by <表示式>。在 by 後面的表示式是該 委託, 因為屬性對應的 get()(和 set())會被委託給它的 getValue() 和 setValue() 方法。 屬性的委託不必實現任何的介面,但是需要提供一個 getValue() 函式(和 setValue()——對於 var 屬性)。

而Lazy.kt檔案中,宣告瞭Lazy介面的getValue擴充套件函式。故在最終賦值的時候會呼叫該方法。

@kotlin.internal.InlineOnly
//返回初始化的值。
public inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value
複製程式碼

五、靜態內部類式

//Java實現
public class SingletonDemo {
    private static class SingletonHolder{
        private static SingletonDemo instance=new SingletonDemo();
    }
    private SingletonDemo(){
        System.out.println("Singleton has loaded");
    }
    public static SingletonDemo getInstance(){
        return SingletonHolder.instance;
    }
}
//kotlin實現
class SingletonDemo private constructor() {
    companion object {
        val instance = SingletonHolder.holder
    }

    private object SingletonHolder {
        val holder= SingletonDemo()
    }

}
複製程式碼

靜態內部類的實現方式,也沒有什麼好說的。Kotlin與Java實現基本雷同。

補充

在該篇文章結束後,有很多小夥伴諮詢,如何在Kotlin版的Double Check,給單例新增一個屬性,這裡我給大家提供了一個實現的方式。(不好意思,最近才抽出時間來解決這個問題)

class SingletonDemo private constructor(private val property: Int) {//這裡可以根據實際需求發生改變
  
    companion object {
        @Volatile private var instance: SingletonDemo? = null
        fun getInstance(property: Int) =
                instance ?: synchronized(this) {
                    instance ?: SingletonDemo(property).also { instance = it }
                }
    }
}
複製程式碼

其中關於?:操作符,如果 ?: 左側表示式非空,就返回其左側表示式,否則返回右側表示式。 請注意,當且僅當左側為空時,才會對右側表示式求值。

觀察程式碼我們可以發現大致上和我們的Java中的Double check是一樣的。

最後

附上我寫的一個基於Kotlin 仿開眼的專案SimpleEyes(ps: 其實在我之前,已經有很多小朋友開始仿這款應用了,但是我覺得要做就做好。所以我的專案和其他的人應該不同,不僅僅是簡單的一個應用。但是,但是。但是。重要的話說三遍。還在開發階段,不要打我),歡迎大家follow和start

相關文章