kotlin object關(guān)鍵字單例模式實(shí)現(xiàn)示例詳解
正文
object 關(guān)鍵字有三種不同的語義:匿名內(nèi)部類、伴生對象、單例模式。因?yàn)?Kotlin 的設(shè)計(jì)者認(rèn)為,這三種語義本質(zhì)上都是在定義一個類的同時還創(chuàng)建了對象。在這樣的情況下,與其分別定義三種不同的關(guān)鍵字,還不如將它們統(tǒng)一成 object 關(guān)鍵字。
一、 匿名內(nèi)部類
Android中用java寫View的點(diǎn)擊事件:
findViewById(R.id.tv).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//do something
}
});
在 Kotlin 當(dāng)中,我們會使用 object 關(guān)鍵字來創(chuàng)建匿名內(nèi)部類。同樣,在它的內(nèi)部,我們也必須要實(shí)現(xiàn)它內(nèi)部未實(shí)現(xiàn)的方法。這種方式不僅可以用于創(chuàng)建接口的匿名內(nèi)部類,也可以創(chuàng)建抽象類的匿名內(nèi)部類:
findViewById<TextView>(R.id.tv).setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View?) {
//do something
}
})
//上面的代碼可以用SAM轉(zhuǎn)換簡化,IDE會提示
Java 和 Kotlin 相同的地方就在于,它們的接口與抽象類,都不能直接創(chuàng)建實(shí)例。想要創(chuàng)建接口和抽象類的實(shí)例,我們必須通過匿名內(nèi)部類的方式。
在 Kotlin 中,匿名內(nèi)部類還有一個特殊之處,就是我們在使用 object 定義匿名內(nèi)部類的時候,其實(shí)還可以在繼承一個抽象類的同時,來實(shí)現(xiàn)多個接口:
//抽象類和抽象方法
abstract class Person{
abstract fun isAdult()
}
//接口
interface AListener {
fun getA()
}
//接口
interface BListener {
fun getB()
}
//繼承一個抽象類的同時,來實(shí)現(xiàn)多個接口
private val item = object :Person(),AListener,BListener{
override fun isAdult() {
//do something
}
override fun getA() {
//do something
}
override fun getB() {
//do something
}
}
在日常的開發(fā)工作當(dāng)中,我們有時會遇到這種情況:我們需要繼承某個類,同時還要實(shí)現(xiàn)某些接口,為了達(dá)到這個目的,我們不得不定義一個內(nèi)部類,然后給它取個名字。但這樣的類,往往只會被用一次就再也沒有其他作用了。所以針對這種情況,使用 object 的這種語法就正好合適。我們既不用再定義內(nèi)部類,也不用想著該怎么給這個類取名字,因?yàn)橛眠^一次后就不用再管了。
引申:可以把函數(shù)當(dāng)做參數(shù)簡化定義接口的操作。以前寫java時應(yīng)該都寫過很多如下的接口回調(diào):
class DownloadFile {
//攜帶token下載文件
fun downloadFile(token:String) {
val filePath = ""
listener?.onSuccess(filePath)
}
//定義成員變量
private var listener: OnDownloadResultListener? = null
//寫set方法
fun setOnDownloadResultListener(listener: OnDownloadResultListener){
this.listener = listener
}
//定義接口
interface OnDownloadResultListener {
fun onSuccess(filePath:String)
}
}
通過函數(shù)當(dāng)做參數(shù)就不需要定義接口了:
class DownloadFile {
private var onSuccess: ((String?) -> Unit)? = null
fun downloadFile(token:String) {
val filePath = ""
onSuccess?.invoke(filePath)
}
fun setOnDownloadResultListener(method:((String?) -> Unit)? = null){
this.onSuccess = method
}
}
//調(diào)用
DownloadFile().downloadFile("")
DownloadFile().setOnDownloadResultListener { filePath ->
print("$filePath")
}
二、單例模式
在 Kotlin 當(dāng)中,要實(shí)現(xiàn)單例模式其實(shí)非常簡單,我們直接用 object 修飾類即可:
object StringUtils {
fun getLength(text: String?): Int = text?.length ?: 0
}
//反編譯
public final class StringUtils {
@NotNull
public static final StringUtils INSTANCE; //靜態(tài)單例對象
public final int getLength(@Nullable String text) {
return text != null ? text.length() : 0;
}
private StringUtils() {
}
static { //靜態(tài)代碼塊
StringUtils var0 = new StringUtils();
INSTANCE = var0;
}
}
這種方式定義的單例模式,雖然簡潔,但存在兩個缺點(diǎn):
1、不支持懶加載。
2、不支持傳參構(gòu)造單例。寫構(gòu)造方法會報(bào)錯,會提示object修飾的類不允許有構(gòu)造方法。
三、伴生對象
1、深入分析伴生對象
Kotlin 當(dāng)中沒有 static 關(guān)鍵字,所以我們沒有辦法直接定義靜態(tài)方法和靜態(tài)變量。不過,Kotlin 還是為我們提供了伴生對象,來幫助實(shí)現(xiàn)靜態(tài)方法和變量。
我們先來看看 object 定義單例的一種特殊情況,看看它是如何演變成“伴生對象”的:
class User() {
object InnerClass {
fun foo() {}
}
}
用object修飾嵌套類,看下反編譯的結(jié)果:
public final class User {
//object修飾的內(nèi)部類為靜態(tài)內(nèi)部類
public static final class Inner {
@NotNull
public static final User.Inner INSTANCE; //靜態(tài)單例對象
public final void foo() {
}
private Inner() {
}
//通過static靜態(tài)代碼塊創(chuàng)建了單例對象
static {
User.Inner var0 = new User.Inner();
INSTANCE = var0;
}
}
}
調(diào)用的時候的代碼
User.InnerClass.foo()
可以看到foo方法并不是靜態(tài)方法,那加上@JvmStatic這個注解試試:
class User() {
object InnerClass {
@JvmStatic
fun foo() {}
}
}
//反編譯結(jié)果
public final class User {
public static final class InnerClass {
@NotNull
public static final User.InnerClass INSTANCE;
@JvmStatic
public static final void foo() { //foo方法變成了靜態(tài)方法
}
private InnerClass() {
}
static {
User.InnerClass var0 = new User.InnerClass();
INSTANCE = var0;
}
}
}
foo方法變成了一個靜態(tài)方法,但是在使用的時候還是要User.InnerClass.foo(),而User類中的靜態(tài)方法應(yīng)該是直接User.foo()調(diào)用才對,這還是不符合定義靜態(tài)方法的初衷。那在 Kotlin 如何實(shí)現(xiàn)這樣的靜態(tài)方法呢?我們只需要在前面例子當(dāng)中的 object 關(guān)鍵字前面,加一個 companion 關(guān)鍵字即可。
①不加@JvmStatic注解
//假如不加@JvmStatic注解
class User() {
companion object InnerClass {
fun foo() {}
}
}
//反編譯
public final class User {
@NotNull
public static final User.InnerClass InnerClass = new User.InnerClass((DefaultConstructorMarker)null);
public static final class InnerClass {
public final void foo() {
}
private InnerClass() {
}
// $FF: synthetic method
public InnerClass(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
//調(diào)用
User.foo()
//反編譯調(diào)用的代碼
User.InnerClass.foo();
如果不加上@JvmStatic注解調(diào)用的時候只是省略了前面的單例對象InnerClass,foo仍然不是User的靜態(tài)方法。
②加@JvmStatic注解
//假如加@JvmStatic注解
class User() {
companion object InnerClass {
@JvmStatic
fun foo() {}
}
}
//反編譯
public final class User {
@NotNull
public static final User.InnerClass InnerClass = new User.InnerClass((DefaultConstructorMarker)null);
@JvmStatic
public static final void foo() { //多生成了一個foo方法,但其實(shí)還是調(diào)用的下面的foo方法
InnerClass.foo();
}
public static final class InnerClass {
@JvmStatic
public final void foo() { //實(shí)際的foo方法
}
private InnerClass() {
}
// $FF: synthetic method
public InnerClass(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
可以看到這個時候多生成了一個靜態(tài)的foo方法,可以通過User.foo()真正去調(diào)用了,而不是省略掉了InnerClass單例對象(把InnerClass對象放在了靜態(tài)方法的實(shí)現(xiàn)中)。
那又有問題來了,上面二種方式應(yīng)該如何選擇,哪種情況下哪個好,什么時候該加注解什么時候不該加注解?
解析:1、用companion修飾的對象會創(chuàng)建一個Companion的實(shí)例:
class User {
companion object {
fun foo() {}
}
}
//反編譯
public final class User {
@NotNull
public static final User.Companion Companion = new User.Companion((DefaultConstructorMarker)null);
public static final class Companion {
public final void foo() {
}
private Companion() {
}
// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
//java中調(diào)用
User.Companion.foo();
如果不加@JvmStatic,java調(diào)用kotlin代碼會多創(chuàng)建這個Companion實(shí)例,會多一部分內(nèi)存開銷,所以如果這個靜態(tài)方法java需要調(diào)用,那務(wù)必要把@JvmStatic加上。
2、多創(chuàng)建一個靜態(tài)foo方法會不會多內(nèi)存開銷? 答案是不會,因?yàn)檫@個靜態(tài)的foo方法調(diào)用的也是Companion中的方法foo方法,所以不會有多的內(nèi)存開銷。
2、用伴生對象實(shí)現(xiàn)工廠模式
所謂的工廠模式,就是指當(dāng)我們想要統(tǒng)一管理一個類的創(chuàng)建時,我們可以將這個類的構(gòu)造函數(shù)聲明成 private,然后用工廠模式來暴露一個統(tǒng)一的方法,以供外部使用。Kotlin 的伴生對象非常符合這樣的使用場景:
// 私有的構(gòu)造函數(shù),外部無法調(diào)用
class User private constructor(name: String) {
companion object {
@JvmStatic
fun create(name: String): User? {
// 統(tǒng)一檢查,比如敏感詞過濾
return User(name)
}
}
}
3、用伴生對象實(shí)現(xiàn)單例模式
(1)、借助懶加載委托
class MainActivity : AppCompatActivity() {
//借助懶加載委托實(shí)現(xiàn)單例
private val people by lazy { People("張三", 18) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
//反編譯后
public final class MainActivity extends AppCompatActivity {
private final Lazy people$delegate;
private final People getPeople() {
Lazy var1 = this.people$delegate;
Object var3 = null;
return (People)var1.getValue();
}
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(1300000);
}
public MainActivity() { //構(gòu)造方法
this.people$delegate = LazyKt.lazy((Function0)null.INSTANCE); //lazy方法有線程安全的實(shí)現(xiàn)
}
}
在MainActivity的構(gòu)造方法中通過LazyKt.lazy獲取類的代理對象,看下LazyKt.lazy的源碼實(shí)現(xiàn):
/**
* Creates a new instance of the [Lazy] that uses the specified initialization function [initializer]
* and the default thread-safety mode [LazyThreadSafetyMode.SYNCHRONIZED]. //線程安全模式
*
* If the initialization of a value throws an exception, it will attempt to reinitialize the value at next access.
*
* Note that the returned instance uses itself to synchronize on. Do not synchronize from external code on
* the returned instance as it may cause accidental deadlock. Also this behavior can be changed in the future.
*/
public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
/**
* Creates a new instance of the [Lazy] that uses the specified initialization function [initializer]
* and thread-safety [mode].
*
* If the initialization of a value throws an exception, it will attempt to reinitialize the value at next access.
*
* Note that when the [LazyThreadSafetyMode.SYNCHRONIZED] mode is specified the returned instance uses itself
* to synchronize on. Do not synchronize from external code on the returned instance as it may cause accidental deadlock.
* Also this behavior can be changed in the future.
*/
public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
when (mode) {
LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
}
(2)、伴生對象 Double Check
class UserManager private constructor(name: String) {
companion object {
@Volatile
private var INSTANCE: UserManager? = null
fun getInstance(name: String): UserManager =
// 第一次判空
INSTANCE?: synchronized(this) {
// 第二次判空
INSTANCE?:UserManager(name).also { INSTANCE = it }
}
}
}
// 使用
UserManager.getInstance("Tom")
我們定義了一個伴生對象,然后在它的內(nèi)部,定義了一個 INSTANCE,它是 private的,這樣就保證了它無法直接被外部訪問。同時它還被注解“@Volatile”修飾了,這可以保證INSTANCE的可見性,而getInstance()方法當(dāng)中的synchronized,保證了INSTANCE的原子性。因此,這種方案還是線程安全的。
同時,我們也能注意到,初始化情況下,INSTANCE 是等于 null 的。這也就意味著,只有在getInstance() 方法被使用的情況下,我們才會真正去加載用戶數(shù)據(jù)。這樣,我們就實(shí)現(xiàn)了整個UserManager的懶加載,而不是它內(nèi)部的某個參數(shù)的懶加載。
另外,由于我們可以在調(diào)用getInstance(name) 方法的時候傳入初始化參數(shù),因此,這種方案也是支持傳參的。
單例模式最多的寫法,注意如果參數(shù)是上下文,不能傳遞Activity或Fragment的上下文,不然會有內(nèi)存泄漏。(單例的內(nèi)存泄漏)
(3)、抽象類模板
如果有多個類似于上面的單例,那么就會有很多重復(fù)代碼,于是嘗試抽象成模板代碼:
//要實(shí)現(xiàn)單例類,就只需要繼承這個 BaseSingleton 即可
//P為參數(shù),T為返回值
abstract class BaseSingleton<in P, out T> {
@Volatile
private var instance: T? = null
//抽象方法,需要我們在具體的單例子類當(dāng)中實(shí)現(xiàn)此方法
protected abstract fun creator(param: P): T
fun getInstance(param: P): T =
instance ?: synchronized(this) {
instance ?: creator(param).also { instance = it }
}
}
通過伴生對象實(shí)現(xiàn)抽象類,并給出具體實(shí)現(xiàn)
//構(gòu)建UploadFileManager對象需要一個帶參數(shù)的構(gòu)造方法
class UploadFileManager(val param: String) {
//伴生對象實(shí)現(xiàn)BaseSingleton抽象類
companion object : BaseSingleton<String, UploadFileManager>() {
//重寫方法并給出具體實(shí)現(xiàn)
override fun creator(param: String): UploadFileManager {
return UploadFileManager(param)
}
}
fun foo(){
print("foo")
}
}
//調(diào)用
UploadFileManager.getInstance("張三").foo()
因?yàn)闃?gòu)造方法的限制這種封裝也有一定的局限性。
以上就是kotlin object關(guān)鍵字單例模式實(shí)現(xiàn)示例詳解的詳細(xì)內(nèi)容,更多關(guān)于kotlin object關(guān)鍵字單例模式的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android模擬實(shí)現(xiàn)支付寶螞蟻森林效果
這篇文章主要為大家詳細(xì)介紹了如何利用Android模擬實(shí)現(xiàn)支付寶中螞蟻森林的動畫效果,文中的示例代碼講解詳細(xì),感興趣的可以了解一下2022-09-09
Android WebView實(shí)現(xiàn)截長圖功能
這篇文章主要為大家詳細(xì)介紹了Android截長圖的一種實(shí)現(xiàn)方式,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-05-05
Android中EditText的drawableRight屬性設(shè)置點(diǎn)擊事件
這篇文章主要介紹了Android中EditText的drawableRight屬性的圖片設(shè)置點(diǎn)擊事件,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-10-10
android通過Location API顯示地址信息的實(shí)現(xiàn)方法
這篇文章主要介紹了android通過Location API顯示地址信息的方法,涉及Android操作Geocoder類獲取地址信息的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-07-07

