单例设计模式
学习单例设计模式及其在 Kotlin 中的实现。
单例模式——即使那些不喜欢使用设计模式的人,也一定听说过单例模式。曾经有人甚至将其称为反模式,但那仅仅是因为它过于流行。
那么,对于第一次接触这个模式的人来说,它到底是什么?
通常情况下,当我们有一个类时,可以随意创建多个实例。例如,假设我们要列出我们最喜欢的电影:
1val myFavoriteMovies = listOf("黑鹰坠落", "银翼杀手") 2val yourFavoriteMovies = listOf(...)
注意,我们可以创建任意多个 List 实例,这没有任何问题。大多数类都可以有多个实例。
接下来,如果我们两人都想列出“速度与激情”系列的最佳电影呢?
1val myFavoriteQuickAndAngryMovies = listOf() 2val yourFavoriteQuickAndAngryMovies = listOf()
注意,这两个列表完全相同,因为它们都是空的。而且由于它们是不可变的,所以会一直保持为空。
既然这两个类的实例完全相同,根据 equals 方法判断,就没有必要在内存中保存多个副本。理想情况下,所有指向空列表的引用都应该指向同一个对象实例。事实上,null 就是这种情况——所有的 null 都是相同的。
这正是单例设计模式的核心思想。
要求
单例设计模式有以下几个基本要求:
- 系统中必须只有一个实例。
- 该实例应能从系统的任何地方访问。
在 Java 或其他一些语言中,实现这些要求非常复杂。首先,我们需要将类的构造函数设为 private,以禁止外部创建新实例;其次,还必须确保实例化过程是懒加载、线程安全且性能良好的,具体包括:
- **懒加载**:我们可能不希望程序启动时就创建单例对象,因为这可能是一个开销较大的操作。我们希望只在首次需要时才进行实例化。
- **线程安全**:如果两个线程同时尝试创建单例对象,它们都应获得同一个实例,而不是两个不同的实例。
- **高性能**:如果有大量线程同时尝试创建单例对象,我们不应长时间阻塞它们,否则会严重影响执行效率。
在 Java 或 C++ 中满足所有这些要求非常困难,或者至少代码会显得冗长繁琐。
Kotlin 通过引入一个关键字 object,让创建单例变得极为简单。使用这个关键字,我们可以轻松实现一个满足所有要求的单例对象。
注意:
object关键字不仅仅用于创建单例。我们将在本章后续部分深入探讨它的其他用途。
声明单例对象的方式类似于定义普通类,但不需要构造函数,因为单例对象不能由我们手动实例化:
1object NoMoviesList
从现在起,我们可以在代码的任何位置访问 NoMoviesList,并且系统中只会存在一个该对象的实例:
1val myFavoriteQuickAndAngryMovies = NoMoviesList 2val yourFavoriteQuickAndAngryMovies = NoMoviesList 3println(myFavoriteQuickAndAngryMovies === yourFavoriteQuickAndAngryMovies) // true
注意这里使用的引用相等运算符 ===,用于检查两个变量是否指向内存中的同一对象。但这真的算是一个列表吗?
让我们写一个函数来打印电影列表:
1fun printMovies(movies: List<String>) { 2 for (m in movies) { 3 println(m) 4 } 5}
当我们传入一个初始的电影列表时,代码可以正常编译:
1// 每部电影单独一行输出 2printMovies(myFavoriteMovies)
但如果我们传入我们的空电影列表,代码将无法编译:
1printMovies(myFavoriteQuickAndAngryMovies) 2// 类型不匹配:推断出的类型是 NoMoviesList,但期望的是 List<String>
原因是我们的函数只接受 String 类型列表作为参数,而没有告诉编译器 NoMoviesList 也是这种类型(尽管名字暗示了这一点)。
幸运的是,在 Kotlin 中,单例对象可以实现接口,而 List 是一个泛型接口:
1object NoMoviesList : List<String>
现在,编译器会提示我们实现所需的方法。我们只需为 object 添加一个主体即可:
1object NoMoviesList : List<String> { 2 override val size = 0 3 override fun contains(element: String) = false 4 ... 5}
现在,Kotlin 已经提供了一个用于创建任意类型空列表的函数:
1printMovies(emptyList())
该函数返回一个实现了 List 接口的单例对象。这是一个现代软件中依然活跃应用设计模式的绝佳例子。
Kotlin 的 object 与普通类有一个重要区别——它不能有构造函数。如果需要为单例添加初始化逻辑,比如首次加载配置文件中的数据,可以使用 init 块:
1object Logger { 2 init { 3 println("我第一次被访问了") 4 // 初始化逻辑放在这里 5 } 6 // 更多代码放在这里 7}
完整实现如下:
1fun main() { 2 Logger.log("Hello World") 3} 4 5/** 6 * object 关键字将创建一个单例 7 */ 8object Logger { 9 init { 10 println("我第一次被访问了") 11 12 // 初始化逻辑放在这里 13 } 14 15 fun log(message: String) { 16 println("正在记录 $message") 17 } 18 // 更多代码放在这里 19}
需要注意的是,如果从未调用过该单例,它的初始化逻辑将完全不会执行,从而节省资源。这被称为懒初始化。