Data Class在序列化和反序列化遇到的“坑”

Data Class在序列化和反序列化遇到的"坑"


在 Kotlin 中,data class 是一种特殊的类,专为存储数据而设计。它可以自动生成与数据相关的标准方法(如 equals()、hashCode()、toString() 等),显著减少样板代码。

1. 基本语法与特性

定义

使用 data class 关键字声明,需至少有一个主构造参数,且所有主构造参数必须标记为 val 或 var:

kotlin 复制代码
data class User(
    val id: Int,          // 不可变属性(推荐)
    var name: String,     // 可变属性
    val age: Int = 18     // 带默认值的属性
)

自动生成的方法

编译器会自动为 data class 生成以下方法:

  • equals() 和 hashCode():用于对象比较和哈希操作。
  • toString():格式为 ClassName(property1=value1, property2=value2, ...)。
  • componentN() 函数:按声明顺序对应每个属性,支持解构声明。
  • copy() 函数:用于创建对象的副本,可选择性修改部分属性。

2. 序列化和反序列化(坑)

ps:由于业务实践上的序列化和反序列化一般都是需要服务端通过json构造数据,客户端进行解析。这里为了说明问题,就不采用@SerializedName+mock数据的方式了,直接构造数据

  • 正常的序列化和反序列化例子
    • Data Class

      kotlin 复制代码
      data class Data(
          val name: String = "test",
          val content: String
      )
    • 测试

      kotlin 复制代码
      	fun main() {
          val gson = Gson()
          val data = Data("phc","你好")
          // 序列化:类对象->json string
          val jsonString = gson.toJson(data)
          println(jsonString)
       
          // 反序列化
          val restoreData = gson.fromJson(jsonString,Data::class.java)
          println(restoreData)
      }
    • 输出 这个看起来确实很正常

  • gson()+data class带来的坑
    • Data Class

      kotlin 复制代码
      data class Data(
         val name: String = "test",
         val content: String
      )
    • 测试

      kotlin 复制代码
      fun main() {
          val gson = Gson()
          // 序列化:类对象->json string
          val jsonString = "{\"content\":\"你好\"}" // 没有给name赋值
          println(jsonString)
       
          // 反序列化
          val restoreData = gson.fromJson(jsonString,Data::class.java)
          println(restoreData)
          println(restoreData.name) // 输出默认值 null,因为 name 没有在 JSON 中提供
      }
    • 输出

      这里大家可能就有疑问了:我明明在Data Class中对name赋予了默认值"test",只是在json中没有给name值(在实际场景中可能会存在服务端对部分字段不返回的情况),为什么输出的name还是null呢?这要是不小心使用了Data里的name,不得来个NPE崩溃?!

      这里查阅了相关资料:默认值不会生效的原因是 Gson 的工作机制与 Kotlin 的默认参数不兼容。具体分析如下:

2.1 问题原因

  • Gson 的反序列化逻辑: Gson 通过 反射调用无参构造函数来创建对象,然后直接设置字段值。对于 JSON 中不存在的字段,Gson 会将其设为 null,不会触发 Kotlin 的默认参数机制。
  • Kotlin 默认参数的工作方式: Kotlin 的默认参数仅在 构造函数调用时 生效。但 Gson 不通过构造函数创建对象,而是直接操作字段,因此默认参数被忽略。

Gson是通过反射来构造对象的,当这个类有无参构造函数时,便会通过无参构造函数 实例化对象,当这个类没有无参构造函数时,便会通过Unsafe.allocateInstance生成对象 如上图所示,我们之前Data Class的写法都不会生成无参构造函数,所以Gson反序列化对象时自然也不会触发默认赋值逻辑。只会触发Unsafe.allocateInstance分配内存、然后通过反射给字段赋值;碰到 JSON 里没有的字段,就干脆不赋,Java 层面的对象字段就保持原生的 null。

2.2 解决方案

1. 使用 Kotlin 反射适配器(推荐)

添加 gson-kotlin 依赖,它提供了 Kotlin 支持:

kotlin 复制代码
implementation("com.google.code.gson:gson:2.10.1")
implementation("com.google.code.gson:gson-kotlin:2.10.1")  // 新增依赖

然后创建 Gson 实例时注册 Kotlin 适配器:

kotlin 复制代码
val gson = GsonBuilder()
    .registerTypeAdapterFactory(KotlinReflectionFactory())  // 关键
    .create()
 
val restoreData = gson.fromJson(jsonString, Data::class.java)
println(restoreData.name)  // 输出 "test"(默认值生效)
2. 使用 Kotlinx Serialization(替代 Gson)

Kotlinx Serialization 是官方库,对 Kotlin 特性支持更好:

kotlin 复制代码
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
 
@Serializable
data class Data(
    val name: String = "test",
    val content: String
)
 
val jsonString = "{\"content\":\"你好\"}"
val data = Json.decodeFromString<Data>(jsonString)
println(data.name)  // 输出 "test"
3. 手动处理默认值(不优雅但可行)

在类中添加初始化逻辑:

kotlin 复制代码
data class Data(
    val name: String?,
    val content: String
) {
    init {
        // 如果 name 为 null,使用默认值
        if (name == null) throw IllegalArgumentException("name cannot be null")
    }
}
4. 每次都给Data Class的成员变量设置默认值,让其可以生成无参构造函数
kotlin 复制代码
data class Data(
    val name: String = "test",
    val content: String = ""
)

Gson vs Kotlinx Serialization

特性 Gson Kotlinx Serialization
Kotlin 默认参数支持 ❌(需额外配置) ✅(原生支持)
空安全处理 ❌(可能产生 NPE) ✅(类型安全)
数据类解构支持
注解支持 有限(需使用 Java 注解) 丰富(Kotlin 专用注解)
性能 一般 优秀

总结

Gson 在反序列化时 不会自动应用 Kotlin 的默认参数值,导致缺失的字段被设为 null。推荐使用 gson-kotlin 适配器或切换到 Kotlinx Serialization 以获得更好的 Kotlin 支持。

Gson 默认并不支持 Kotlin 的构造函数默认值:它直接反射分配、跳过了编译时生成的"带默认值"构造函数,所以缺字段就得不到默认值,而是 null。要么手动补 ?: "test",要么给 Gson 加个 Kotlin-aware 的 TypeAdapterFactory,或者干脆换一个对 Kotlin 默认值支持更好的库(Moshi/Kotlinx.serialization)。这样就能在 JSON 里漏掉 name 字段时,依然拿到 "test" 了。

相关推荐
hnlgzb3 小时前
安卓中,kotlin如何写app界面?
android·开发语言·kotlin
jzlhll1233 小时前
deepseek kotlin flow快生产者和慢消费者解决策略
android·kotlin
wxson72823 小时前
【用androidx.camera拍摄景深合成照片】
kotlin·android jetpack·androidx
jzlhll1234 小时前
deepseek Kotlin Flow 全面详解
android·kotlin·flow
heeheeai4 小时前
kotlin图算法
算法·kotlin·图论
用户097 小时前
Android面试基础篇(一):基础架构与核心组件深度剖析
android·面试·kotlin
Kapaseker12 小时前
每个Kotlin开发者应该掌握的最佳实践,最后一趴
android·kotlin
alexhilton1 天前
灵活、现代的Android应用架构:完整分步指南
android·kotlin·android jetpack
雨白1 天前
初识协程: 为什么需要它以及如何启动第一个协程
android·kotlin
heeheeai1 天前
Kotlinx Serialization 指南
kotlin·序列化