Kotlin Model类在Json反序列化过程为空性探索

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Kotlin Model类在Json反序列化过程为空性探索相关的知识,希望对你有一定的参考价值。

参考技术A

定义一个JsonModel类。

使用Gson类进行反序列化,Gson版本2.8.5

我们看一下反编译出来的Java类,省略不必要的部分。

Boolean类型和Int类型都被编译成了Java原始类型。

这里注意一下 :Kotlin的Boolean、Byte、Short、Int、Long、Float、Double声明为非空类型的时候,最终反编译出来的Java类都会变成对应Java中原始类型:boolean、byte、short、int、long、float、double。而原始类型是都有默认值的,不会为null。

接下来开始探索:

完整的json字符串

如果反序列化的Json字符串没有 show 字段和 number 字段,那么最后反序列化出来的JsonModel对象, show = false , number = 0 。

ReflectiveTypeAdapterFactory.Adapter的read方法。

注释1处,循环判断是否还有下一个值需要处理。处理完string字段以后,json字符串中就没有其他要处理的字段了,也就是说,在Json字符串没有 show 字段和 number 字段的时候,根本不会处理这两个字段,所以都是默认值, show = false , number = 0 。

如果反序列化的Json字符串 show 字段和 number 字段都为 null ,那么最后反序列化出来的JsonModel对象, show = false , number = 0 。

ReflectiveTypeAdapterFactory.Adapter的read方法的注释2处,使用BoundField读取字段。

注释1处,如果是boolean类型,对应的变量是TypeAdapters.BOOLEAN,如果值为null的话,TypeAdapters.BOOLEAN返回的值是null。如果是int类型,对应的变量是TypeAdapters.INTEGER,如果值为null的话,TypeAdapters.INTEGER返回的值是null。

注释2处,条件不满足,所以Java原始类型变量如果对应的json字符串为null的话,最终反序列化的结果就是默认值, show = false , number = 0 。

注释1处,boolean类型变量,如果从json字符串中读取的值是null,返回null

int类型的适配器同理,如果从json字符串中读取的值是null,返回null,那么int类型的变量默认值就是0。

如果反序列化的Json字符串 string 字段缺失,那么在反序列化过程中就不会处理 string 字段,那么 string 字段就是默认值,在这个例子中我们没有给 string 字段赋默认值,所以默认值就是null,那么最后反序列化出来的JsonModel对象, string = null 。

注意:
注意:
注意:

如果我们如下所示,声明JsonModel类,给string字段默认赋值为"你好呀"。

反编译后的Java类,省略无关部分。

我们看到,JsonModel类没有默认的 无参构造函数 。并且只有当调用JsonModel三个参数的构造函数的时候,才会给string字段赋值。

当反序列化的Json字符串 string 字段缺失,反序列化后string字段会默认是"你好呀"吗?并不是。Gson在反序列化过程中要么通过调用 无参构造函数 来构造对象,或者通过 UnsafeAllocator 类,在不调用构造函数的情况下地分配对象。

所以如上声明方式,即使给string字段默认赋值为"你好呀"。在Json字符串string字段缺失的情况下,反序列化之后,string字段值依然为null。这里一定要注意!!!

如果反序列化的Json字符串 string 字段为 null ,那么最后反序列化出来的JsonModel对象, string = null 。

TypeAdapters.STRING

注释1处,值为null,返回null。

也就是说,对于一个引用类型的变量,如果Json字符串中该变量对应的值为null,那么反序列化出来的引用类型变量的值就是null。注意:并且会覆盖该变量的默认值。在这个例子中,我们如果在声明的时候为 string 字符指定一个默认值,但是当 json 字符串中 string 字段对应的值为 null 的时候,最后序列化出来的结果仍然为 null 。

所以正确的做法是把引用类型的变量声明为可空类型。如下所示:

反编译出来的Java类,对应的原始类型都变成了相应的包装类,默认值都是null。所以使用的时候要注意判断是否为null。

这种声明类型是不合适的,将可以不为null的Java基本数据类型,变为了可空的包装类型,使用的时候会增加空判断的逻辑。

Kotlin中Json的序列化与反序列化 -- GsonMoshi

Kotlin中Json的序列化与反序列化 – Gson、Moshi

在App的开发中避免不了需要和Json格式的数据打交道,这节我们来看下Json相关的序列化反序列化的内容。同时注意我们使用Kotlin来进行示例,来进一步理解下Kotlin的空安全设计。

实体类

这里我们准备两个实体类,Car和Driver。购买汽车会随机赠送一个驾驶员:

data class Car(
    val brand: String,      //汽车品牌
    val driver: Driver,    	//随车赠送驾驶员
)
data class Driver(
    val name: String,
    val age: Int,
)

注意:
1、kotlin的数据类;
2、参数为非空类型;

以上Car对象传参的时候brand和driver参数都不可为null。如果要强行给brand或者driver参数赋值为null,那么就会收到编辑器的错误提示信息:
Snipaste_2021-05-31_14-08-02.png

那假如需要给参数传递null值的情况下该怎么处理呢?给参数类型后添加 ? 即可,如下,那么传参的时候都可以传递null进去了:

data class Car(
    val brand: String?,     	//汽车品牌
    val driver: Driver?,    	//随车赠送驾驶员
)

那么使用的时候我们可以使用if-else判空来处理,或者使用 ?. 或者 ?.let{} 操作符来调用,好了,这就是kotlin表面上的空安全设计了。


集成方式

Gson

Gson的Github地址为:https://github.com/google/gson

implementation("com.google.code.gson:gson:2.8.7")

Moshi

Moshi的Github地址为:https://github.com/square/moshi

implementation("com.squareup.moshi:moshi:1.12.0")
kapt("com.squareup.moshi:moshi-kotlin-codegen:1.12.0")

Moshi的集成多了kapt的codegen组件,该组件可以通过注解来生成相关解析的代码,同时性能也有提升,建议使用该方式。


非空类型

注意:这里使用Car的非空数据实体。

序列化

在非空类型下,Gson和Moshi的序列化使用方式上都很简单,我们使用如下Car实例,分别使用Gson和Moshi来进行序列化:

val carBean = Car(
    brand = "Benz",
    driver = Driver(
        name = "XiaoMing",
        age = 20
    )
)

//Gson序列化
val gson = Gson()
val toJson = gson.toJson(carBean)
Log.e("Gson", "toJson ==> $toJson")

//Moshi序列化
val moshi = Moshi.Builder().build()
val toJson = moshi.adapter(Car::class.java).toJson(carBean)
Log.e("Moshi", "toJson ==> $toJson")

日志打印分别如下:

Gson: toJson ==> {"brand":"Benz","driver":{"age":20,"name":"XiaoMing"}}

Moshi: toJson ==> {"brand":"Benz","driver":{"name":"XiaoMing","age":20}}

反序列化

首先,Koltin使用Moshi进行反序列化我们需要给相应的实体类添加注解,也就是给Car和Driver数据类添加如下代码:

@JsonClass(generateAdapter = true)

我们将json数据放到asset文件夹下,然后读取出来并使用Gson、Moshi来分别进行反序列化,asset文件夹下的data.json文件如下:

{
  "brand": "Benz"
}

这里我们没有返回driver的相关数据,那么在Kotlin设置了非空参数类型的情况下,解析会出什么问题呢,请带着该疑问继续阅读下文?

读取该文件并转换为字符串的代码如下:

fun getJsonStr(context: Context): String {
  val inputStream = context.assets.open("data.json")
  val inputStreamReader = InputStreamReader(inputStream)

  val bufferedReader = BufferedReader(inputStreamReader)

  val stringBuilder = StringBuilder()
  var line: String
  while (true) {
    line = bufferedReader.readLine() ?: break
  	stringBuilder.append(line)
  }
  bufferedReader.close()

  return stringBuilder.toString()
}

我们分别使用Gson和Moshi来进行解析:

//获取到Json字符串
val jsonStr = getJsonStr(this)

//Gson解析
val gson = Gson()
val fromJson = gson.fromJson(jsonStr, Car::class.java)
Log.e("Gson", "fromJson ==> $fromJson")
Log.e("Gson", "fromJson.Driver.name ==> ${fromJson.driver.name}")

//Moshi解析
val moshi = Moshi.Builder().build()
val fromJson = moshi.adapter(Car::class.java).fromJson(jsonStr)
Log.e("Moshi", "fromJson ==> $fromJson")
Log.e("Moshi", "fromJson.Driver.name ==> ${fromJson?.driver?.name}")

在使用Gson解析的情况下,输出信息的时候出现了崩溃,打印日志如下:

Gson: fromJson ==> Car(brand=Benz, driver=null)
AndroidRuntime: FATAL EXCEPTION: main
java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String xxx.Driver.getName()' on a null object reference

首先是第一行解析后的Car对象的打印日志,Car的数据类型中我们定义driver参数是不能为null的,结果经过gson解析后却被赋值为null了,好像绕过了Kotlin的空安全机制。然后就下下一步解析获取driver.name的时候不出意外的出现了NullPointerException。

再看Moshi的解析情况,在打印Driver.name的时候,所有的参数都需要我们判空,但是解析还是出现了崩溃,打印日志如下:

com.squareup.moshi.JsonDataException: Required value 'driver' missing at $

由于Car的driver参数不可为空,所以直接在解析阶段就出现了JsonDataException。

但是如果我们给driver设置默认值后,会有什么情况呢?

@JsonClass(generateAdapter = true)
data class Car(

    val brand: String,      //汽车品牌

    val driver: Driver = Driver(
        name = "Default",
        age = 18
    ),    					//随车赠送驾驶员
)

还是使用上文不包含driver的json数据,最后成功解析出包含了默认数据的实例:

Moshi: fromJson ==> Car(brand=Benz, driver=Driver(name=Default, age=18))
Moshi: fromJson.Driver.name ==> Default

结论

在非空数据类型下,如果参数没有设置默认值,那么由于Json数据的不规范:

  • Gson会解析出可能包含null数据的对象,从而绕过了Kotlin的空安全机制,导致调用null对象的时候未判空而崩溃。
  • Moshi则直接在解析的时候就报错崩溃了。

而在【非空参数】设置了【默认值】的情况下,Moshi会成功解析,并使用默认的参数值。Gson还是会解析出null对象。


可空类型

注意:这里我们使用driver参数可为空的Car实体。

序列化

我们使用可空的driver参数来进行示例,如下:

val carBean = Car(
    brand = "Benz",
    driver = null,
)

//Gson序列化
val gson = Gson()
val toJson = gson.toJson(carBean)
Log.e("Gson", "toJson ==> $toJson")

//Moshi序列化
val moshi = Moshi.Builder().build()
val toJson = moshi.adapter(Car::class.java).toJson(carBean)
Log.e("Moshi", "toJson ==> $toJson")

日志打印分别如下:

Gson: toJson ==> {"brand":"Benz"}

Moshi: toJson ==> {"brand":"Benz"}

反序列化

还是使用上文的json字符串,我们解析试下:

//获取到Json字符串
val jsonStr = getJsonStr(this)

//Gson解析
val gson = Gson()
val fromJson = gson.fromJson(jsonStr, Car::class.java)
Log.e("Gson", "fromJson ==> $fromJson")
Log.e("Gson", "fromJson.Driver.name ==> ${fromJson.driver?.name}")//在调用name的时候,drivier需要使用?.的操作符

//Moshi解析
val moshi = Moshi.Builder().build()
val fromJson = moshi.adapter(Car::class.java).fromJson(jsonStr)
Log.e("Moshi", "fromJson ==> $fromJson")
Log.e("Moshi", "fromJson.Driver.name ==> ${fromJson?.driver?.name}")

这时候由于driver参数可为空,所以在调用driver对象的时候,就必须要求使用 ?. 的方式了。

日志打印分别如下:

Gson: fromJson ==> Car(brand=Benz, driver=null)
Gson: fromJson.Driver.name ==> null

Moshi: fromJson ==> Car(brand=Benz, driver=null)
Moshi: fromJson.Driver.name ==> null

但是如果driver参数允许为空,同时我们又设置了默认值的情况下会有什么结果呢?

@JsonClass(generateAdapter = true)
data class Car(

    val brand: String,      //汽车品牌

    val driver: Driver? = Driver(//和之前相比,现在是可空的参数
        name = "Default",
        age = 18
    ),    					//随车赠送驾驶员
)

日志打印分别如下:

Gson: fromJson ==> Car(brand=Benz, driver=null)
Gson: fromJson.Driver.name ==> null

Moshi: fromJson ==> Car(brand=Benz, driver=Driver(name=Default, age=18))
Moshi: fromJson.Driver.name ==> Default

结论

如果相关参数允许为空,那么也就完全用到了Kotlin的空安全机制。
如果相关参数设置了默认值,Moshi会解析为默认值,Gson还是会解析为null。

总结

从基础的集成及使用上来说,两者都很方便。
但是从Kotlin的角度上来说的话,Moshi还是略胜Gson一筹,支持空安全以及默认值,这可以让我们的程序更加健壮。

以上是关于Kotlin Model类在Json反序列化过程为空性探索的主要内容,如果未能解决你的问题,请参考以下文章

Kotlin中Json的序列化与反序列化 -- GsonMoshi

Kotlin中Json的序列化与反序列化 -- GsonMoshi

如何在 Kotlin 中解析 JSON?

Kotlin 反序列化任何不支持的类型

如何使用 Gson @SerializedName 注释在 Kotlin 中反序列化嵌套的 Json API 响应

Kotlin Gson反序列化