data是kotlin的数据类,Gson是最常用的json序列化框架,现在探讨二者的兼容性问题

Gson版本更新到最新

越高的版本兼容kotlin数据类型的概率越高,所以升级到最新版本,当前分析时Gson的版本为
implementation 'com.google.code.gson:gson:2.8.7'

分析Gson的反序列化data

实体bean

  1. data class Partner(
  2. var name: String,
  3. val attack: Int, // 攻击力
  4. val defence: Int, // 防御力
  5. val criticalRate: Float = 0.05f, // 暴击率(0-100%),默认5%
  6. val criticalDMG: Float = 0.5f, // 暴击伤害 (0-∞),默认50%
  7. var comeOnTheStage: Boolean, // 当前是否出场
  8. var prologue: String? // 开场白
  9. )

构造数据

  1. fun m1(){
  2. val amber = Partner("Amber", 206, 189, 0.32f, 1.88f, true, "侦查骑士,登场!")
  3. val amberJson = gson.toJson(amber)
  4. println(amberJson)
  5. }

上面获取到的数据提出成String

  1. const val json1 = """
  2. {"name":"Amber","attack":206,"defence":189,"criticalRate":0.32,"criticalDMG":0.88,"comeOnTheStage":true,"prologue":"侦查骑士,登场!"}
  3. """

反序列化完整数据

  1. fun m2(){
  2. val bean1 = gson.fromJson(json1, Partner::class.java)
  3. println(bean1)
  4. }

Partner(name=Amber, attack=206, defence=189, criticalRate=0.32, criticalDMG=0.88, comeOnTheStage=true, prologue=侦查骑士,登场!)

结论:

  1. 一个完整数据的反序列化是没有问题的

反序列化不完整数据

构造一个不完整的数据

  1. const val json2 = """
  2. {"attack":206,"defence":189,"criticalDMG":0.88,"comeOnTheStage":true,"prologue":"侦查骑士,登场!"}
  3. """

去掉了一个不可为null的name字段,去掉一个没有默认值的defence字段,去掉一个含有默认值的criticalRate字段
反序列化之后:

  1. fun m2(){
  2. val bean1 = gson.fromJson(json2, Partner::class.java)
  3. println(bean1)
  4. }

Partner(name=null, attack=206, defence=0, criticalRate=0.0, criticalDMG=0.88, comeOnTheStage=true, prologue=侦查骑士,登场!)

表现:

  1. name字段被映射成null,在kotlin中出现了null-safe的问题
  2. defece字段被映射成0,对应java的基本数据类型被赋值为默认值
  3. criticalRate字段被映射成0.0f,是java中float类型的默认值,而当前data中的默认值会丢失

分析Gson反序列化

Gson的反序列化过程:

  1. 查找固定类型的解析器
    1. 一般通过这个解决String、int这种通用类型赋值的问题,比如将(null 改为 “”)
  2. 走空参数的构造函数
    1. data数据没有空参数的构造函数
  3. Unsafe类构造bean,不需要走构造函数
    1. data数据是通过这种方式创建的,所以构造函数的默认赋值不生效

解决反序列化的空安全问题

data中所有字段都标记为Nullable,即加上?

  1. data class Partner2(
  2. var name: String?,
  3. val attack: Int?, // 攻击力
  4. val defence: Int?, // 防御力
  5. val criticalRate: Float? = 0.05f, // 暴击率(0-100%),默认5%
  6. val criticalDMG: Float? = 0.5f, // 暴击伤害 (0-∞),默认50%
  7. var comeOnTheStage: Boolean?, // 当前是否出场
  8. var prologue: String? // 开场白
  9. )

但是这样使用起来不方便,而且很多必须不为null的字段,比如某个boolean值,为null无法判断。

结合上面java的8种基本数据类型反序列化会赋默认值,那么data中所有的非基本数据类型必须加上?来接收

  1. data class Partner3(
  2. var name: String?,
  3. val attack: Int, // 攻击力
  4. val defence: Int, // 防御力
  5. val criticalRate: Float = 0.05f, // 暴击率(0-100%),默认5%
  6. val criticalDMG: Float = 0.5f, // 暴击伤害 (0-∞),默认50%
  7. var comeOnTheStage: Boolean, // 当前是否出场
  8. var prologue: String? // 开场白
  9. )

这样就不会出现空安全问题,而且用起来也比较方便
结合gson的解析构造器,可以给String等类型赋默认值,必然不为null的类型更多

解决data类默认值丢失的问题

Unsafe构造的data必然会丢失data上的默认值,必须走构造函数才能赋值,结合Gson反序列化,会调用无参构造函数,那么就提供一个无参的构造函数

  1. data class Partner4(
  2. var name: String,
  3. val attack: Int, // 攻击力
  4. val defence: Int, // 防御力
  5. val criticalRate: Float = DEFAULT_CORTICAL_RATE, // 暴击率(0-100%),默认5%
  6. val criticalDMG: Float = DEFAULT_CORTICAL_DMG, // 暴击伤害 (0-∞),默认50%
  7. var comeOnTheStage: Boolean = false, // 当前是否出场
  8. var prologue: String? // 开场白
  9. ) {
  10. companion object {
  11. const val DEFAULT_CORTICAL_RATE = 0.05f
  12. const val DEFAULT_CORTICAL_DMG = 0.5f
  13. }
  14. constructor() : this("", 0, 0, DEFAULT_CORTICAL_RATE, DEFAULT_CORTICAL_DMG, false, null)
  15. }

这样写起来比较麻烦,但是是通过构造函数去生成类,而不是走Unsafe,稳定性更高,而且带有data默认值

代码如下

  1. const val json3 = """
  2. {"attack":206,"criticalDMG":0.88,"prologue":"侦查骑士,登场!"}
  3. """
  4. fun m2(){
  5. val bean1 = gson.fromJson(json3, Partner4::class.java)
  6. println(bean1)
  7. }

Partner4(name=, attack=206, defence=0, criticalRate=0.05, criticalDMG=0.88, comeOnTheStage=false, prologue=侦查骑士,登场!)

没有出现null-safe问题,默认值手动赋上

最终解决方案

  1. kotlin data类中的非java基本类型(包含能保证no-null的类型),必须加上?类标记nullable
  2. 提供一个空参数构造函数,手动赋默认值,然后由反序列化的解析数据覆盖

具体使用哪一种,可以按照接口的复杂度自由选择

补充:

如果某个字段在反序列化中不存在,那么反序列化依然会有nullsafe问题

  1. const val json3 = """
  2. {"name":null,"attack":206,"criticalDMG":0.88,"prologue":"侦查骑士,登场!"}
  3. """
  4. const val json4 = """
  5. {"attack":206,"criticalDMG":0.88,"prologue":"侦查骑士,登场!"}
  6. """

如上面代码,json4比json3少一个name字段

使用TypeAdapter创建Gson

  1. class StringAdapter : TypeAdapter<String>() {
  2. override fun write(out: JsonWriter?, value: String?) {
  3. out?.let { jsonWriter ->
  4. value?.let {
  5. jsonWriter.value(it)
  6. } ?: jsonWriter.nullValue() // 此方法不会写该字段
  7. }
  8. }
  9. override fun read(`in`: JsonReader?): String {
  10. `in`?.let {
  11. if (it.peek() == JsonToken.NULL) { // 如果返回nulL,转为""
  12. it.nextNull()
  13. return ""
  14. }
  15. return it.nextString()
  16. }
  17. return ""
  18. }
  19. }

解析json3结果为

Partner(name=, attack=206, defence=0, criticalRate=0.0, criticalDMG=0.88, comeOnTheStage=false, prologue=侦查骑士,登场!)

解析json4结果为

Partner(name=null, attack=206, defence=0, criticalRate=0.0, criticalDMG=0.88, comeOnTheStage=false, prologue=侦查骑士,登场!)

结论:

  1. 使用TypeAdapter可以解决字段值为null的默认赋值问题
  2. 使用TypeAdapter不能解决字段不存在的问题