
Kotlin 中的 object
是声明单例的标准方式------即每个 JVM 创建一个单一的、全局可访问的实例。
这种保证是在语言层面上的。但在实际项目中,这种保证可能会失效------而且不会有编译器错误或明显的警告。
一个常见的原因是序列化。一些库在反序列化过程中会返回一个新的实例,这就破坏了引用相等性以及共享状态。
本文将解释 Kotlin 单例在什么情况下不再是单例,以及在实践中如何避免这种情况。
大多数开发者在 Kotlin 中把object
用作语言层面的单例。它只创建一次,持有全局状态,并且在整个应用中被一致引用。
但这种保证只适用于同一个类加载器中,并且只有在直接使用该对象时才有效。像序列化器这样的运行时工具可能会在毫无警告的情况下破坏这种行为。
Gson

当使用 Gson
对 Kotlin 对象进行序列化和反序列化时:
kotlin
object MySingleton {
const val NAME: String = "MySingleton"
}
fun main() {
val gson = Gson()
// 序列化
val json = gson.toJson(MySingleton)
// 反序列化后的 MySingleton
val deserialized = gson.fromJson(json, MySingleton::class.java)
println("MySingleton before serialization hashCode: ${System.identityHashCode(MySingleton)}")
println("MySingleton after serialization hashCode: ${System.identityHashCode(deserialized)}")
println("Same instance: ${deserialized === MySingleton}")
}
输出:
yaml
// Output
MySingleton before serialization hashCode: 399534175
MySingleton after serialization hashCode: 428910174
Same instance: false
尽管原始类型是 object
,但 Gson
在反序列化时会创建一个新的实例。引用相等性丧失了,单例中持有的任何全局状态也没有被保留下来。
为什么会这样?
Gson
无法识别 Kotlin 的 object
。它将其视为一个带有字段的普通类。在反序列化过程中,它使用反射创建一个新的实例------尽管 object
原本是被设计为单例的。
这并不是 Gson
的错误。这是 Kotlin 特有的结构与一个在 Java 类层面工作的库之间的不匹配。
需要记住的是:
- Kotlin 的
object
在语言层面上保证每个类加载器只有一个实例。 - 像
Gson
这样的序列化库将其视为一个普通类,并在反序列化时创建新的实例。 - 如果对象的一致性很重要,在使用
object
进行序列化时就需要显式处理。 - 为了保留单例行为,可以使用一个自定义的适配器,它始终返回原始实例。
但是 kotlinx.serialization
会更加聪明。
kotlinx.serialization

Kotlin 的官方序列化库能够识别 object
。当用 @Serializable
注解时,反序列化器知道它是一个单例,并返回现有的实例。
kotlin
@Serializable
object MySingleton {
const val NAME: String = "MySingleton"
}
// 目前的版本,当需要使用 serializer() 扩展时,需要引入该 OptIn。
@OptIn(InternalSerializationApi::class)
fun main() {
val json = Json { encodeDefaults = true }
// 序列化
val serialized = json.encodeToString(MySingleton)
// 反序列化
val deserialized = json.decodeFromString(MySingleton::class.serializer(), serialized)
println("MySingleton before serialization hashCode: ${System.identityHashCode(MySingleton)}")
println("MySingleton after serialization hashCode: ${System.identityHashCode(deserialized)}")
println("Same instance: ${deserialized === MySingleton}")
}
输出:
yaml
// Output
MySingleton before serialization hashCode: 399534175
MySingleton after serialization hashCode: 399534175
Same instance: true
这种行为是有意为之。Kotlin 序列化插件理解 object
声明,并能安全地复用了已存在的实例。
那么,最后一个序列化库 Moshi
是如何处理 Kotlin 的 object
?
Moshi

Moshi
采取了更严格的方法。当被要求对 Kotlin 的 object
进行序列化或反序列化时,它不会创建新的实例,而是抛出一个异常。
kotlin
object MySingleton {
const val NAME: String = "MySingleton"
}
fun main() {
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
val adapter = moshi.adapter(MySingleton::class.java)
val json = adapter.toJson(MySingleton)
val deserialized = adapter.fromJson(json)
}
输出:
php
// Output
Exception in thread "main" java.lang.IllegalArgumentException:
Cannot serialize object declaration com.example.MySingleton
这种行为可以防止意外的重复实例。要使用 Moshi
对 Kotlin 的object
进行反序列化,必须编写一个自定义的适配器,它始终返回原始实例。
总结
Kotlin 的 object
作为单例在语言层面上运行是可靠的。
但在涉及序列化时,这种保证可能会被破坏。
Gson
在反序列化时创建一个新实例Moshi
抛出错误并拒绝序列化kotlinx.serialization
保留原始实例
果然,官方才是亲儿子!
如果你的应用依赖于对象一致性、引用相等性或共享状态------在将 object
与第三方序列化器结合使用时要格外小心。
在以 Kotlin 为主的项目中,处理 object
时优先使用 kotlinx.serialization
。