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,那么就会收到编辑器的错误提示信息:
那假如需要给参数传递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