Gson
是 Google
提供的一个用于在 Java 对象和 JSON 数据之间进行序列化和反序列化的开源库,拥有广泛的社区支持和相对丰富的文档资源,靠着Google
这棵大树和其简单直观的API,一直以来稳坐Android
开发序列化框架的头把交椅。
哪怕是Google 在 2017 年的 Google I/O 开发者大会上宣布 Kotlin 成为 Android 官方开发语言后,Gson依然作为大部分Android项目的序列化框架使用 ------ 即使语言从Java切换成了Kotlin,即使Gson是一款专为Java语言开发的序列化库。
是的,很长一段时间内,Kotlin都没有自己的序列化库。
用着用着,苦逼的Android开发者发现了不对劲。
空安全不安全
Kotlin 出道时就将空安全性
作为自已一大卖点到处宣扬,通过引入可空类型和非空类型来实现空安全性。
在 Kotlin 中,变量默认情况下是非空的,如果一个变量可能为空,需要在类型后面添加一个问号(
?
)来表示这个变量是可空的。可空类型可以避免在使用可能为空的变量时出现空指针异常:eg:
var nullableString: String? = null
然而Kotlin这套引以为傲的空安全机制,在使用Gson反序列化时却出了问题,我们通过一个例子来说明:
kotlin
data class User(
val name: String,
val nickName: String,
)
数据User
类包含两个属性,name
和nickName
,他们均声明为不可空String
类型,我们使用Gson反序列化下面这段json
数据:
json
{ "name": "Donald Trump" }
得到结果为User(name=Donald Trump, nickName=null)
,原本不可空的nickName
被赋值成了null
,空安全变得不再安全。如果此时代码中使用nickName
,就会得到一个大大的NPE
[\悲伤]。
失效的默认值
还是以User
类为例,实际开发中,不是所有的用户都会为自己设置昵称nickName
的,我们希望用户没有设置nickName
时展示一个默认值,代码我们往往会这么写:
kotlin
data class User(
val name: String,
val nickName: String = "我是川普",
)
还是上面那段json
数据,还是使用Gson解析,得到的结果仍然超出了我们的意料:
kotlin
val json =
"""
{ "name": "Donald Trump" }
"""
val user = Gson().fromJson(json, User::class.java)
// 结果:User(name = Donald Trump, nickName = null)
实际情况就是我们设置的默认值nickName = "我是川普"
并没有生效,仍然被置为空了,这暴露了Gson反序列data class时的另一个问题:主构造函数中字段的默认值被忽略,完全失效了。
why?
发现问题后我们不禁要产生疑问,为什么?为什么空安全不安全了?为什么默认值失效了?
这些问题产生的原因归根结底和 Gson 序列化的原理有关,或者说和Gson设计的目标相关。
我们都知道Gson是针对Java 对象和 JSON 数据之间进行序列化和反序列化所设计的,Java语言中有一个JavaBean
的概念,在Java语言中封装数据类通常都遵循JavaBean
的规范和约定:
- 公共无参数构造函数:JavaBean 类必须提供一个公共的无参数构造函数,这是为了确保 Java 反射机制可以实例化该类。
- 私有属性:JavaBean 类通常将属性设置为私有的,并通过公共的 getter 和 setter 方法来访问和修改属性的值。
Gson在将JSON文本反序列化为Java对象时,会根据对象类型的不同,使用不同的方式创建对象:
- 基础类型、以及基础类型的包装类型等由Gson提供的TypeAdapter通过 new 关键字创建;
- 枚举类型在EnumTypeAdapter中只是通过枚举名称切换不同的枚举常量,不涉及对象的创建;
- 集合和map等容器类型通过Gson内置的对象创建工厂,调用 new 关键字进行创建;
- Java Bean对象的创建比较复杂,分为3种情况,优先级由上到下依次降低:
- 开发者定义了对象创建工厂
InstanceCreator
,则使用该工厂创建; - 存在默认的无参构造函数,通过反射构造函数创建;
- 使用
Unsafe API
创建。
- 开发者定义了对象创建工厂
如果对Gson的工作原理好奇的,可以查看「赏码」更优雅的使用Gson解析Json
由于Kotlin 的 data class
没有默认的无参构造函数,Gson会使用 Unsafe API
来创建对象,这种创建对象的方式不会调用构造函数,因此会导致了以下三个问题:
- 默认值丢失;
- Kotlin 非空类型失效;
- 初始化块可能不会正常执行;
如何解决
Kotlin 官方有一个NoArg
插件,这个插件允许在Kotlin类的主构造函数中自动添加无参构造函数,以解决Gson
等序列化框架因没有默认的无参构造函数导致反序列化过程中的一系列问题。
NoArg
插件虽然解决了data class
没有默认无参构造函数的问题,但还是会有默认值失效的问题。
有人会说那我声明时这么处理不就好了么:
kotlin
data class User(
val name: String? = null,
val nickName: String? = null,
)
兄弟,因噎废食听过么?
Kotlin官方显然也注意到了这个问题,于是kotlinx.serialization
应运而生,专为kotlin语言设计,全平台通用。
kotlinx.serialization
不同于Gson
使用反射进行反序列化,kotlinx.serialization
使用了KSP来完成序列化以及反序列化工作,因此理论上性能也会更好些,完成这些只需要将data class
加上@Serializable
注解即可:
kotlin
@Serializable
data class User(
val name: String,
val nickName: String = "我是川普",
)
经过编译后的代码大概就是这样:
kotlin
public final class User {
@NotNull
public static final Companion Companion = new Companion((DefaultConstructorMarker)null);
@NotNull
private final String name;
@NotNull
private final String nickName;
public static final int $stable;
public User(@NotNull String name, @NotNull String nickName) {
Intrinsics.checkNotNullParameter(name, "name");
Intrinsics.checkNotNullParameter(nickName, "nickName");
super();
this.name = name;
this.nickName = nickName;
}
// 省略 构造函数、setter/getter、component、copy、toString等data class自动生成的函数...
public static final class $serializer implements GeneratedSerializer {
@NotNull
public static final $serializer INSTANCE = new $serializer();
// $FF: synthetic field
private static final PluginGeneratedSerialDescriptor descriptor;
public static final int $stable;
private $serializer() {
}
@NotNull
public SerialDescriptor getDescriptor() {
return (SerialDescriptor)descriptor;
}
@NotNull
public KSerializer[] childSerializers() {
KSerializer[] var1 = new KSerializer[]{StringSerializer.INSTANCE, StringSerializer.INSTANCE};
return var1;
}
// 反序列化代码
@NotNull
public User deserialize(@NotNull Decoder decoder) {
Intrinsics.checkNotNullParameter(decoder, "decoder");
SerialDescriptor var2 = this.getDescriptor();
boolean var3 = true;
boolean var4 = false;
int var5 = 0;
String var6 = null;
String var7 = null;
CompositeDecoder var8 = decoder.beginStructure(var2);
if (var8.decodeSequentially()) {
var6 = var8.decodeStringElement(var2, 0);
var5 |= 1;
var7 = var8.decodeStringElement(var2, 1);
var5 |= 2;
} else {
while(var3) {
int var9 = var8.decodeElementIndex(var2);
switch (var9) {
case -1:
var3 = false;
break;
case 0:
var6 = var8.decodeStringElement(var2, 0);
var5 |= 1;
break;
case 1:
var7 = var8.decodeStringElement(var2, 1);
var5 |= 2;
break;
default:
throw new UnknownFieldException(var9);
}
}
}
var8.endStructure(var2);
return new User(var5, var6, var7, (SerializationConstructorMarker)null);
}
// 序列化代码
public void serialize(@NotNull Encoder encoder, @NotNull User value) {
Intrinsics.checkNotNullParameter(encoder, "encoder");
Intrinsics.checkNotNullParameter(value, "value");
SerialDescriptor var3 = this.getDescriptor();
CompositeEncoder var4 = encoder.beginStructure(var3);
User.write$Self$app_debug(value, var4, var3);
var4.endStructure(var3);
}
// ...
}
}
kotlinx.serialization VS Gson
请允许我举几个例子来说明为什么kotlinx.serialization
相比于Gson
更适合kotlin:
默认值
一个简单的🌰:
kotlin
@Serializable
data class User(
val name: String,
val nickName: String = "我是川普",
)
val jsonString = """{ "name": "Donald Trump" }"""
val objByGson = Gson().fromJson(jsonString, User::class.java) // 结果 User(name="Donald Trump", nickName=null)
val objByKotlinxSerialization = Json.decodeFromString<User>(jsonString)// 结果 User(name="Donald Trump", nickName="我是川普")
不出意外的,kotlinx.serialization
对于构造方法中的默认值进行了正确处理,而Gson直接忽略了默认值的存在。
空安全
还是一个简单的🌰:
kotlin
@Serializable
data class User(
val name: String,
val nickName: String,
)
val jsonString = """{ "name": "Donald Trump" }"""
val objByGson = Gson().fromJson(jsonString, User::class.java) // 结果 User(name="Donald Trump", nickName=null)
val objByKotlinxSerialization = Json.decodeFromString<User>(jsonString)// ❌ 反序列化失败,抛出kotlinx.serialization.MissingFieldException
为保证Kotlin的空安全性,在将JOSN 字符串反序列化成对象时,如果某个不可空字段在JSON字符串中缺失,Kotlinx.serialization
将会反序列化失败,抛出kotlinx.serialization.MissingFieldException
异常,而Gson则会将其反序列化成null
------ 这显然破坏了Kotlin引以为傲的空安全特性。
kotlinx.serialization
的这一特性会让我们在声明data class
时更加严谨,你需要对字段的可空与否有自己的一番思考 ------ 这显然比无脑将字段声明为可空类型要好的多得多。当然,如果你还是担心由此引发的崩溃问题,同时又不想后续的使用上加上一堆?.
的可空调用判断,你可以给其赋上默认值:
kotlin
@Serializable
data class User(
val name: String,
val nickName: String = "我是川普",
)
总结
历史的车轮滚滚前进,我们不得不承认「长江后浪推前浪」
的客观事实,比如我们今天讲的JSON与数据类对象的转换:Java语言层面是没有任何数据类的设计,只有一个Java Bean的概念(Java 17推出了Record类来弥补这个不足):JavaBean要有无参构造函数、属性要私有、要有对应的getter/setter方法,这就要求使用者思考构造函数的继承性以及一个类该如何被正确的实例化,然而这种依赖开发者「自觉性」的行为显然是靠不住的。
如果手头有维护的Java项目,看一眼就知道了,在这点上,Kotlin显然要做的更好:它强制你思考一个类该如何正确初始化。