当Object遇到Json你可能会碰到的坑

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。

相关推荐
Goway_Hui12 小时前
【KMP for OpenHarmony】-- 在鸿蒙工程中使用 Apollo-Kotlin
华为·kotlin·harmonyos
释厄62314 小时前
AI 军备竞赛悖论——为 AI 卖游戏但 AI 需要游戏母场景
人工智能·游戏·kotlin
D e re k ZH15 小时前
保序回归:从 PAVA 到可并堆的单调约束优化
c++·算法·数据挖掘·回归·kotlin
Goway_Hui16 小时前
【KMP for OpenHarmony】Cache4k 0.14.0 鸿蒙适配
华为·kotlin·harmonyos
YB137517 小时前
kotlin与函数式编程
kotlin
我是唐青枫1 天前
Kotlin Delegation 委托详解
开发语言·kotlin
唐青枫2 天前
Kotlin Inheritance(继承)详解:语法、原理、实战 Demo
kotlin
alexhilton2 天前
用Konsist与Lint强制执行设计系统契约
android·kotlin·android jetpack
唐青枫2 天前
Kotlin Delegation 委托详解
kotlin
传奇开心果4 天前
Compose Multiplatform 共享边界实战:架构契约、互操作同步与可回退设计
android·kotlin