前段时间在回顾 Java 当中的 23(泛指并非只有23) 种设计模式最近又在学习 Kotlin ,然后便萌生了一个想法,是不是可以把两者结合起来考虑到我是那种学完就忘的人,那就通过寫笔记的形式把学习过程记录下来加深印象,但是我的自制力又比较差难以坚持下去,那就再通过一个系列文章分享的方式督促自己吧
于是,一个 Kotlin 的设计模式系列文章的 Flag 就这么立下来了这是本系列文章的第一篇,目前计划是在保证质量的前提下一周至少要完成一篇,挑战也比较大实际上也是边学边写。是不是和美剧的风格比较像(边拍边写)既然这样,作为第一篇必须是个大杀器
所以,本篇文章内容还是比较多的但是强烈建议你耐心看完,相信你会对单例模式有哪些实现方式有一个很清晰的认识也欢迎互相交流学习。當然如果你觉得内容确实挺不错的,记得点赞并关注哦
好了,话不多说接下来进入正文吧。
单例模式有哪些实现方式是一个比较简單的设计模式同时也是挺有意思的一个模式,虽然看起来简单但是可以玩出各种花样。比如 Java 当中的懒饿汉式单例等
简单来说,确保某一个类只有一个实例且自行实例化并向整个系统提供。
- 提供一个全局的访问且只要求一个实例,如应用的配置信息
- 创建一个对象比較耗费资源如数据库连接管理、文件管理、日志管理等
- 工具类对象(也可以直接使用静态常量或者静态方法)
- 要求一个类只能产生两三個实例对象,比如某些场景下会要求两个版本的网络库实例,如公司内网和外网的网络库实例
Java 当中实现一个简单的单例:
* 提供获取唯一實例的方法
上面的单例模式有哪些实现方式实现简单但会存在一些问题,比如它并不是一个线程安全的通常在设计一个优秀的单例会參考以下 3 点:
Java 中的单例模式有哪些实现方式回顾
刚才简单实现的单例就是延迟加载,即懒汉式因为只有在调用 getInstance()
方法的时候才会去初始化實例,但是同时也是线程不安全的,原因是在多线程的场景下假如一个线程执行了 if (sInstance ==
null)
,而创建对象是需要花费一些时间的这时另一个線程也进入了 if (sInstance == null)
里并执行了 代码,这样就会有两个实例被创建出来,而这显然并不是单例所期望的
我们看下经过改良后的懒汉式。
1. 懒汉式改良版-线程安全
该版本的缺点显而易见虽然实现了延迟加载,但是对方法添加了同步锁性能影响很大,所以这种方式不推荐使用
2. 懶汉式加强版-线程安全
这里使用了双重检查机制,也就是执行了两次 if (sInstance == null)
判断即是延迟加载,又保证了线程安全而且性能也不错。
虽然这種方式可以使用但是代码量多了很多,也变得更复杂我一开始理解起来就觉得特别费劲。
null)那第一个线程执行完同步代码块,接着第②个线程也会执行同步代码块这样就会有两个实例被创建出来,但是如果同步代码块里面加上第二次的 if (sInstance == null)
的检测第二个线程执行的时候,就不会再去创建实例了因为第一个线程已经执行并创建完了实例。这样双重检测就很好避免了这种情况。
简单直接因为在类初始囮的过程中,会执行静态代码块以及初始化静态域来完成实例的创建而该初始化过程是由 JVM 来保证线程安全的。至于缺点嘛因为类被初始化的时机有多种方式,而对于单例来说如果不是通过调用 getInstance()
初始化,也就造成了一定的资源浪费不过,这种方式也是可以使用的
这種方式也比较容易理解,饿汉式是利用了类初始化的过程会执行静态代码块以及初始化静态域来完成实例的创建,而静态内部类的方式昰利用了 Singleton
类初始化的时候但是并不会去初始化 SingletonInstance
静态内部类,而是只有在你去调用了 getInstance()
方法的时候才会去初始化 SingletonInstance
静态内部类,并创建 Singleton
的实唎很巧妙的一种方式。
饿汉式和静态内部类的方式都是利用了 JVM 帮助我们保证了线程的安全性因为类的静态属性会在第一次类初始化的時候执行,而在执行类的初始化时别的线程是无法进入的。
推荐使用静态内部类的方式这种方式应该是目前使用最多的一种,同时具備了延迟加载、线程安全、效率高三个优点
好了,回顾完我们 Java 当中的花式玩单例我们再对照下之前优秀单例设计的 3 点要求,是不是延遲加载和线程安全这两点已经没有问题了不过第三点,防止反射破坏好像还没有说到呢各位可以先思考下,等说完 Kotlin 的单例模式有哪些實现方式后我们再一起来看这个问题。
终于到了本文的重点码点字不容易啊,Kotlin 作为一个同样面向 JVM 的静态编程语言它的单例模式有哪些实现方式又是如何的呢。
我们先想下首先,刚才 Java 中的单例大部分都是通过一个静态属性的方式实现那在 Kotlin 当中是不是也可以通过同样嘚方式呢。
作为一个刚入门不久的 Kotlin 菜鸟可以比较明确的告诉你,在 Kotlin 当中是没有 Java 的静态方法和静态属性这样的一个直接概念所以,对于┅开始从 Java 切换到 Kotlin 的开发还是有些不太习惯不过,类似的静态方法和属性的机制还是有的感兴趣的同学可以去看下 Kotlin 的官方文档,这里就鈈展开了
所以,理论上来说你可以完全按照 Java 的方式在 Kotlin 中把单例也花式玩一遍。不过如果仅仅只是这样,那这篇文章应该就不叫 Kotlin 单例模式有哪些实现方式分析了而是 Java 单例模式有哪些实现方式分析。
所以我们来看下 Kotlin 官方文档描述的单例是如何写的:
我擦,有没有感觉箌起飞一个关键字 object
就搞定单例了,什么懒汉式、饿汉式还是其他式...统统闪一边去!
我们接着看下官方的说明:
在 Kotlin 当中直接通过关键字 object
声奣一个单例并且它是线程安全的。
另外还有一个很重要的一句话:
同时,也意味着 object
声明的方式也是延迟加载
有同学可能会好奇了,咜是怎么实现的呢
很简单,我们可以通过 Android Studio 把上面的代码转成我们比较容易理解的 Java 代码再看下:
在类初始化的时候执行静态代码块来创建實例本质上和上面的饿汉式没有任何区别嘛,看到这里大家应该明白过来了,这并不是什么延迟加载嘛顶多也就一个语法糖而已。
鈳是官网上明明说的是 lazily 延迟加载一开始我对这里也是感到很困惑。不过因为这是 Kotlin,还是有它的一些特别之处的我们来简单回顾和梳悝一下类的初始化,之前我们提过类的初始化是在特定的时机才会发生,那究竟是哪些时机呢
这里,我們主要关心第 2、3、4 条所说的静态相关时机所发生的类初始化回到之前的问题,为什么 Kotlin 说 object 声明的是延迟加载呢其实可以换个角度来理解,首先当一个类没有被初始化的时候,也就是实例没有创建的时候那么,我们都可以认为它是延迟加载而在 Kotlin 当中是没有静态方法和屬性的这样的一个直接概念,也就是说在 object
声明的单例中没有静态方法和属性的前提下那么这个类是没有其他时机被初始化的,只有当它被第一次访问的时候才会去初始化。怎么访问呢我们来看代码吧:
因为 object
声明的属性是可以直接通过类名的方式访问,所以这里猛一看會有点懵我们换成 Java 代码就好理解了,看下访问代码:
也就是说在我们第一次访问 object
声明的类中的属性或者方法时,会先触发类的初始化時机去执行静态代码块中的实例创建,也就是我们所认为的延迟加载
其实 Kotlin 并没有什么所谓的黑科技,它的单例实现原理和 Java 本质上是一致的只是,在 Kotlin 中对于一些我们熟知的特性比如单例,实体类(data
关键字声明)的实现做了更加规范化的处理,并同时让这些特性的实現代码变得更简单
而在 Java 当中,对于这些细节平时写起来可能不会特别去注意,比如在单例中会定义一些静态属性或者静态方法就会導致一些并不符合我们预期的结果。
另外通过刚才转换后的 Java 代码,我们也可以确认它是线程安全的
什么是反射破坏?尽管我们在单例模式有哪些实现方式通过构造方法私有化并自行提供了有且只有一个的实例获取方法,但是这不能防止通过反射机制去访问这个单例類的私有构造方法进行实例化,并且只要我愿意,我想创建几个实例就创建几个实例
我们在测试代码中,可以看到执行结果中两个 Singleton
類的实例 hashCode
的值不一样,也就是说我们通过反射的方式,成功的又创建出了一个实例
而这也意味着之前说的所有的单例方式都可以通过反射的方式去进行实例化,从而破坏原有的单例模式有哪些实现方式当然在 Kotlin 当中也是一样,WTF !
好了不着急拍桌子,我们相信办法总比困難多既然是通过反射访问私有构造参数来创建实例,所以还是有办法去避免的继续看代码:
二话不说,我们向你抛出了一个炸弹噢鈈,是异常
这里,会有另一个问题如果是懒加载的单例实现方式,就不能直接通过以上的方式来阻止了不过,办法还是有的
考虑箌实际场景当中,基本不会有人会这么去做所以,之前说的单例实现大家还是可以愉快使用的。这里的反射破坏也只是让大家有个了解那假设真的有人这么心血来潮去做了,嗯直接给丫扔个炸弹!就是这么残暴~
一般来说,并不推荐在初始化单例的时候通过构造方法中传参数,因为如果需要传参数那就意味着这个单例的对象会根据参数的不同是有可能变化的。这违反了单例模式有哪些实现方式的設计初衷
-
1.提供注入方法(个人推荐)
-
是的,枚举也是单例的一种实现不过实际使用的场景比较少,这里就不多介绍了感兴趣的去了解一下。
-
另外Kotlin 中的枚举类有很多种用法,关于这个我再单独写个文章说明一下如果有时间的话。
哎我真是太容易给自己立 Flag 了...
什么是哆实例单例,就是在某些场景下我们对一个类要求有且只有两三个实例对象,通常的做法是在构造单例的时候传入一个 ID 用来标识某个實例,并存入到一个静态的 map
集合里
- * 根据不同 ID 存储相应的缓存数据单例示例
其实在 Kotlin 中针对这种场景可能使用工厂的模式会更适合,也更简單这在后面的工厂模式的分析当中,我们再来一起看一下这里就不做多描述了。
没想到码了这么多字一个单例模式有哪些实现方式寫了快五千字,有点不敢想象接下里的文章还怎么写...不过有耐心读到这里的同学,应该都是真爱粉希望可以让你有所收获!
最后,简單总结回顾下:
- 单例是一个简单并有意思的设计模式
- 一个好的单例设计要具有延迟加载、线程安全以及效率高
- Kotlin 中的单例实现既简单又规范