Kotlin单例陷阱

Kotlin的object关键字是我们声明单例的标准方式——一个全局可访问的实例,每个JVM只创建一次。这在语言层面是有保证的。但在实际项目中,这个保证可能会被打破——而且不会有任何编译器错误或可见警告。

为什么你的单例不再是单例?

最常见的"凶手"就是序列化。某些库在反序列化时会返回一个新实例,破坏了引用相等性和共享状态。

Gson是如何破坏Kotlin单例的

大多数开发者使用Kotlin的object作为语言级别的单例。它只创建一次,持有全局状态,并在整个应用中保持一致引用。

但这个保证只适用于同一个类加载器——而且只有在直接使用对象时才有效。像序列化器这样的运行时工具可以在没有任何警告的情况下破坏这种行为。

看看当你用Gson序列化和反序列化一个Kotlin object时会发生什么:

object MySingleton {
    const val NAME: String = "MySingleton"
}

fun main() {
    val gson = Gson()
    // 序列化
    val json = gson.toJson(MySingleton)
    // 反序列化
    val deserialized = gson.fromJson(json, MySingleton::class.java)
    
    println("序列化前MySingleton的hashCode: ¥{System.identityHashCode(MySingleton)}")
    println("序列化后MySingleton的hashCode: ¥{System.identityHashCode(deserialized)}")
    println("是否是同一个实例: ¥{deserialized === MySingleton}")
}

输出结果:

序列化前MySingleton的hashCode: 399534175 
序列化后MySingleton的hashCode: 428910174 
是否是同一个实例: false

尽管原始类型是object,Gson在反序列化时创建了一个新实例。引用相等性丢失了,单例内部持有的任何全局状态也不会被保留。

为什么会这样?

Gson并不"认识"Kotlin的object。它把它当作一个有字段的普通类。在反序列化过程中,它使用反射创建一个新实例——即使object被设计为单例。

这不是Gson的bug,而是Kotlin特定构造与在Java类级别工作的库之间的不匹配。

如何正确保持单例行为

kotlinx.serialization如何保持对象标识

Kotlin的官方序列化库确实能识别object。当用@Serializable注解时,反序列化器知道它是一个单例——并返回现有实例。

@Serializable
object MySingleton {
    const val NAME: String = "MySingleton"
}

@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的hashCode: ¥{System.identityHashCode(MySingleton)}")
    println("序列化后MySingleton的hashCode: ¥{System.identityHashCode(deserialized)}")
    println("是否是同一个实例: ¥{deserialized === MySingleton}")
}

输出结果:

序列化前MySingleton的hashCode: 399534175 序列化后MySingleton的hashCode: 399534175 是否是同一个实例: true

这种行为是设计使然。Kotlin序列化插件理解object声明并安全地重用现有实例。

Moshi如何处理Kotlin object

Moshi采取了更严格的方法。它不是创建一个新实例,而是在要求序列化或反序列化Kotlin object时抛出异常。

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)
}

输出结果:

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。

转自:震惊Kotlin单例翻车实录

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐