Kotlin作为Android的官方开发语言已经有一段时间了,在项目过程中总结了一些Kotlin下处理Json的经验,记录下来和大家分享。
Kotlin的数据类
Kotlin中,有一种只保存数据的类,称作数据类。关键字是class前加一个data。例如:
1 | data class User(val name: String, val age: Int) |
一切看起来很美好,接触过Kotlin的同学肯定知道,普通类下的var变量或val常量,都会有恼人的初始化提示,尤其是类的成员变量(var),如果不给予初始化赋值,那么就需要手动添加lateinit关键字,如果初始化null,那么类型就变成了xx?, 对于Bean类而言,这些都是累赘。Data类正好解决了我们的问题。但是Kotlin的Data类在搭配Gson使用时,真的那么美好吗?看下一个问题。
Data class的字段可否为空的问题
首先我们来看一个例子:
1 | data class User( |
这里我们看到,name,age,data 我们都申明为非空,但是实际上,这样的申明是没有多大用处的,举个最简单的例子:
1 | println(Gson().fromJson("", User::class.java)) |
输出结果:
1 | User(name='null', age=0, data=null) |
显然,除了Int类型的age之外,其它两个变量全都是null,这样Kotlin赋予的空指针检查也就无法使用了。
那么一种方式是我们把字段申明为可空,也就是:
1 | data class User( |
这是一种思路,但很多时候我们希望能设置一个默认值,于是我们按照Java的写法,尝试字符串默认值为空;
1 | data class User( |
还是上面的例子,
1 | println(Gson().fromJson("", User::class.java)) |
结果输出的是:
1 | User(name='null', age=0, data=null) |
居然没有生效,name原本的默认空值也被覆盖为null了, 这个问题网上也讨论了很多,其实是Gson这个库的处理方式导致的,在Gson的fromJson方法中,最终会尝试通过反射的方式,获取对象的无参构造函数去创建对象。简单来说,我们这里的User类,实际上他的构造函数有两个,分别是:
User(age: Int, data: List
User(name: String, age: Int,data: List
而显然,并没有无参的构造函数,而Gson这里的反射构造对象,会绕过构造函数,只会在堆中去分配一个对象实例。
解决方式目前来看,就是如果需要默认值,那么所有参数都设置默认值:
1 | data class User( |
这样User相当于有一个User()的无参构造函数了,这样我们的初始化配置也会生效。
具体可以参考这篇博客:Gson 反序列化 Kotlin 数据类默认值失效
Json序列化空字符串如何当做null处理?
某些时候我们在将对象转成Json String的过程中,需要特别处理某些空字符串。例如拼接到url后面的参数,如果字符串为空,就不拼到参数当中。例如:
1 | data class Params( |
输出结果:
1 | //预期 |
这里其实是可以通过自定义序列化的方式来解决的,也就是将空字符串当做null来处理:
1 | class EmptyStringAsNullTypeAdapter: JsonSerializer<String> { |
如何统一过滤Array中的null元素?
数组里的null元素是一个很坑的事情,通常情况下,除非后台在数组中明确返回了null元素之外,是不太可能在数组中存在null的。例如:
1 | data class User( |
显然,在每个数组的声明中都申明为可空想想就是件麻烦的事,而且读取的时候额外的元素判空也是很恶心的一件事。
1 | data class User( |
当然我们尽量希望后台不要传null在数组中,但有的时候无法避免这种情况,该怎么处理呢?针对这种反序列化的情况,我们需要自定义JsonDeserializer
1 | class RemoveNullListDeserializer<T> : JsonDeserializer<List<T>> { |
接着问题来了,我们来看一下下面这个做法,能否满足我们的需求吗?
1 | class RemoveNullListDeserializer<T> : JsonDeserializer<List<T>> { |
这里的做法是将当前JsonArray中的null元素移除,但其实这样做是不够的,举个例子大家就明白了
1 | data class A( |
我们这里注册的反序列化的方法,实际上只针对A.list1、A.list2和A.b.values三个元素生效,而A.list2.values是没办法接管到的。原因在于C的values的反序列化在注册的时候被更高层的List
1 | class RemoveNullListDeserializer<T> : JsonDeserializer<List<T>> { |
在Gson中,抽象类JsonElement一共有四种类型,分别是JsonObject, JsonArray, JsonPrimitive, JsonNull.
可能存在数组的地方只可能是JsonArray或JsonObject。这里用了简单的递归,在反序列化数据之前,手动遍历所有可能存在的List,删除null元素。
总结
Kotlin的数据类为我们定义数据类型的类提供了便利的同时,搭配Gson使用的时候,因为Gson采用了反射的方法获取默认的无参构造函数创建对象,如果没有无参构造函数的话,则通过反射直接绕过了构造函数创建对象,所以如果想让默认值生效,则必须提供无参的构造函数。而提供无参的构造函数的本身其实在开发上增加了负担。