在知道单例(Singleton)这个名词之前,你就可能就已经创建或使用过它了。甚至常见到
Kotlin 都为其分配了 object
关键字,专门用于单例的创建。
在 Kotlin 中创建单例十分简单:
object Singleton {
val field = "12312312"
fun foo() {
("Singleton foo invoked")
println}
}
举个例子,Kotlin 的空返回值实质上就是一个名为 Unit
的单例。
public object Unit {
override fun toString() = "kotlin.Unit"
}
不难看出,单例对象只会初始化一次,实质上只占用一块内存:
@Test
fun `unit singleton`() {
fun foo(): Unit = println("foo invoked")
fun bar(): Unit = println("bar invoked")
{ foo() === bar() }
assertTrue }
// Output:
// foo invoked
// bar invoked
// Exit with code 0
不妨看看 Kotlin 在幕后做的:
// 将 .class 文件反编译为 .java
@Metadata(/* ... */)
public final class Singleton {
@NotNull
public static final Singleton INSTANCE = new Singleton();
@NotNull
private static final String field = "12312312";
private Singleton() {
}
public final void foo() {
System.out.println("Singleton foo invoked");
}
@NotNull
public final String getField() {
return field;
}
}
这正是 Java 创建单例的方式,写一个 class,将构造器私有,定义一个名为
INSTANCE
的属性。Kotlin 帮我们省了不少力。 :)
为了减少开销,所有幕后基本类型字段都是 static
的。(幕后字段这一术语将代理和计算属性排除在外。)
同时,不难发现 object
是懒加载(lazy
initialization)的,直到你使用它,它才会被创建。以下代码可以再次印证这一点:
object Sun {
{
init ("Sun appeared in the world")
println}
fun shine() {
("Sun is shining")
println}
}
@Test
fun `lazy load singleton`() {
("World is dark now")
println.shine()
Sun("World is bright now")
println}
// Output:
// World is dark now
// Sun appeared in the world
// Sun is shining
// World is bright now
对于频繁使用或大量创建,但内容不变的类,推荐使用单例模式减少占用。
最经典的莫过于空集合:emptyList()
emptyArray()
emptyMap()
emptySet()
emptyFlow()
等等。
请看 emptyList()
的定义:
public fun <T> emptyList(): List<T> = EmptyList
仅仅是对内部 EmptyList 的一层兼容,继续查看
EmptyList
:
internal object EmptyList : List<Nothing>, Serializable, RandomAccess {
// ...
override fun hashCode(): Int = 1
override fun toString(): String = "[]"
override val size: Int get() = 0
override fun isEmpty(): Boolean = true
// ...
}
可以看出实现了一个空的集合单例。所以,如果你的代码中存在不变且空的数据,也请考虑一下单例模式。
不过上面的例子有一点缺憾,我们的单例并不是线程安全的。在高并发场景下,如果多个类同时访问并尝试创建单例,可能会出现问题。使用 Kotlin 代理(delegate),我们可以轻易实现一个简单、线程安全、懒加载的单例:
class Singleton private constructor() {
companion object {
val INSTANCE by lazy(/* LazyThreadSafetyMode.SYNCHRONIZED */) { Singleton() }
}
}
LazyThreadSafetyMode.SYNCHRONIZED
被注释,因为它是
lazy
代理的默认值。关于代理的细节,可能在后续详细介绍。
然而单例模式有时候会被滥用。例如将 object
用作命名空间以存放方法。实际上这很多余。Kotlin
并不强制你把所有方法都放在某个类下。尽情将它们放到 top-level
并用文件和包区分即可。函数也是一等公民。
标准库就是这么做的,also
let
apply
等通用方法,map
filter
等集合方法,都被定义为 top-level 的拓展函数。