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

相关推荐
alexhilton5 小时前
初探Compose中的着色器RuntimeShader
android·kotlin·android jetpack
小墙程序员5 小时前
kotlin元编程(二)使用 Kotlin 来生成源代码
android·kotlin·android studio
小墙程序员5 小时前
kotlin元编程(一)一文理解 Kotlin 反射
android·kotlin·android studio
KotlinKUG贵州8 小时前
贪心算法:从“瞎蒙”到稳赚
算法·kotlin
Yang-Never12 小时前
Kotlin -> object声明和object表达式
android·java·开发语言·kotlin·android studio
智江鹏14 小时前
Android 之 Kotlin 和 MVVM 架构的 Android 登录示例
android·开发语言·kotlin
旭宇16 小时前
PDF注释的加载和保存功能的实现
android·kotlin
Yang-Never17 小时前
Kotlin协程 ->launch构建协程以及调度源码详解
android·java·开发语言·kotlin·android studio
Kapaseker18 小时前
软件为什么“软”——从Android架构史说起
android·kotlin