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" 了。

相关推荐
Kapaseker19 小时前
一杯美式搞定 Kotlin 空安全
android·kotlin
FunnySaltyFish2 天前
什么?Compose 把 GapBuffer 换成了 LinkBuffer?
算法·kotlin·android jetpack
Kapaseker2 天前
Compose 进阶—巧用 GraphicsLayer
android·kotlin
Kapaseker3 天前
实战 Compose 中的 IntrinsicSize
android·kotlin
A0微声z5 天前
Kotlin Multiplatform (KMP) 中使用 Protobuf
kotlin
alexhilton5 天前
使用FunctionGemma进行设备端函数调用
android·kotlin·android jetpack
lhDream6 天前
Kotlin 开发者必看!JetBrains 开源 LLM 框架 Koog 快速上手指南(含示例)
kotlin
RdoZam6 天前
Android-封装基类Activity\Fragment,从0到1记录
android·kotlin
Kapaseker6 天前
研究表明,开发者对Kotlin集合的了解不到 20%
android·kotlin
糖猫猫cc6 天前
Kite:两种方式实现动态表名
java·kotlin·orm·kite