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

Kotlin 中的 object 是声明单例的标准方式------即每个 JVM 创建一个单一的、全局可访问的实例。

这种保证是在语言层面上的。但在实际项目中,这种保证可能会失效------而且不会有编译器错误或明显的警告。

一个常见的原因是序列化。一些库在反序列化过程中会返回一个新的实例,这就破坏了引用相等性以及共享状态。

本文将解释 Kotlin 单例在什么情况下不再是单例,以及在实践中如何避免这种情况。

大多数开发者在 Kotlin 中把object用作语言层面的单例。它只创建一次,持有全局状态,并且在整个应用中被一致引用。

但这种保证只适用于同一个类加载器中,并且只有在直接使用该对象时才有效。像序列化器这样的运行时工具可能会在毫无警告的情况下破坏这种行为。

Gson

当使用 GsonKotlin 对象进行序列化和反序列化时:

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 无法识别 Kotlinobject。它将其视为一个带有字段的普通类。在反序列化过程中,它使用反射创建一个新的实例------尽管 object 原本是被设计为单例的。

这并不是 Gson 的错误。这是 Kotlin 特有的结构与一个在 Java 类层面工作的库之间的不匹配。

需要记住的是:

  • Kotlinobject 在语言层面上保证每个类加载器只有一个实例。
  • 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 是如何处理 Kotlinobject

Moshi

Moshi 采取了更严格的方法。当被要求对 Kotlinobject 进行序列化或反序列化时,它不会创建新的实例,而是抛出一个异常。

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

这种行为可以防止意外的重复实例。要使用 MoshiKotlinobject进行反序列化,必须编写一个自定义的适配器,它始终返回原始实例。

总结

Kotlinobject 作为单例在语言层面上运行是可靠的。

但在涉及序列化时,这种保证可能会被破坏。

  • Gson 在反序列化时创建一个新实例
  • Moshi 抛出错误并拒绝序列化
  • kotlinx.serialization 保留原始实例

果然,官方才是亲儿子!

如果你的应用依赖于对象一致性、引用相等性或共享状态------在将 object 与第三方序列化器结合使用时要格外小心。

在以 Kotlin 为主的项目中,处理 object 时优先使用 kotlinx.serialization

相关推荐
Kapaseker5 小时前
Compose 进阶—巧用 GraphicsLayer
android·kotlin
Kapaseker1 天前
实战 Compose 中的 IntrinsicSize
android·kotlin
A0微声z3 天前
Kotlin Multiplatform (KMP) 中使用 Protobuf
kotlin
alexhilton4 天前
使用FunctionGemma进行设备端函数调用
android·kotlin·android jetpack
lhDream4 天前
Kotlin 开发者必看!JetBrains 开源 LLM 框架 Koog 快速上手指南(含示例)
kotlin
RdoZam4 天前
Android-封装基类Activity\Fragment,从0到1记录
android·kotlin
Kapaseker4 天前
研究表明,开发者对Kotlin集合的了解不到 20%
android·kotlin
糖猫猫cc5 天前
Kite:两种方式实现动态表名
java·kotlin·orm·kite
如此风景5 天前
kotlin协程学习小计
android·kotlin
Kapaseker5 天前
你搞得懂这 15 个 Android 架构问题吗
android·kotlin