
Kotlin data class 被 gson 解析遇到的坑

此篇文章来自于学习 Gson 和 Kotlin Data Class 的避坑指南
本文的所有代码和结论都经过实际验证和推敲,并且加入了我自己的理解和细节,更多算是对原文作者的补充,像是一个学习笔记,原文写的非常好,但我也希望你能看看我的这篇文章,
因为我对于我底层逻辑的思考很有信心,这里有简单的介绍 学习的本质在于摸透底层逻辑
从一个 NullPointerException 开始
1 | package org.czb |
这里例子输出的结果是
1 | UserBean(userName=null, userAge=26) |
这里初级程序员也能看出来 "userName":null
和 username
的 String
类型声明是不对应的,String
是非空的 String
类型
那么这次 gson 的反序列化,居然在调用 printMsg
的时候(第三步),报出了 NPE(NullPointerException)
我们看看这个 printMsg
,把它反编译成 Java 方法
1 | public static final void printMsg( { String msg) |
可以看到,这里有 checkNotNullParameter
所以才会有 NPE
也就是说,gson 反序列化成功了
那么我们就需要再看 gson 到底是如何绕过非空类型的判断(nullsafe)的
gson 如何绕过 null check
我们在 fromJson
处打个断点,使用 IDEA 的 step into 功能,跳到源码去分析
可以发现,前面都是 fromJson 的重名函数,直到这个 typeAdapter.read(reader)
提示
这里我使用的是 com.google.code.gson:gson:2.12.1
,和原文章的作者应该是不一致的,所以源码也有所区别
我们可以看到 508~526
都是在遍历 JsonReader
,我们稍后再看
505
行是创建了一个累加器,我们看看这是干什么的,使用 step into
进去
可以看到,createAccumulator
这里是调用了 ObjectConstructor
的 construct()
函数
如果你直接从这里 step into 会直接跳转到 newUnsafeAllocator
而这个 lambda 就是 constuct()
,所以这个 newUnsafeAllocator
会返回一个 ObjectConstructor
我们再看看 ObjectConstructor
除了这个实现,还有哪些实现
newSpecialCollectionConstructor
使用特殊集合类的构造函数(EnumSet,EnumMap)。
newDefaultConstructor
使用无参构造函数。
newDefaultImplementationConstructor
回退到默认接口实现,支持一些 Gson 写好的集合类型
最后就是 newUnsafeAllocator
也就是我们确定的,这个 Gson 将这次我们使用的 data class 反序列化的构建对象的方式
注意上面的注释
// Consider usage of Unsafe as reflection, so don't use if BLOCK_ALL // Additionally, since it is not calling any constructor at all, don't use if BLOCK_INACCESSIBLE
考虑使用不安全的反射,因此请不要使用block_all,因为它根本不调用任何构造函数
所以这是一个兜底策略,或者说最终手段
UnsafeAllocator
类似 C 里面的 malloc 这里 Allocator 其实是一个含义————分配器。所以我们可以大概猜测,这里是和 C 类似的内存分配方式。这里其实也是对于中国程序员不友好的地方,英语的本身意思其实多多少少蕴含了一些代码的设计意图。
然后我们在继续推进,看看这里 newUnsafeAllocator
可以看到这里使用 sun.misc.Unsafe
反射初始化了一个对象,然后又使用反射获取了 allocateInstance
,调用它生成了一个 UnsafeAllocator
也就是 ObjectConstructor<T>
而 ObjectConstructor<T>
里面只有一个返回 T
的 construct()
函数,所以 createAccumulator
最终就会返回这个被构造出来对象,并且没有使用 要被反序列化的那个类的任何构造函数,因此就跳过了 null check
使用默认值规避 null 风险???
如果我们给 userName
一个默认值,那么就可以规避掉风险了吗?我们看例子。
1 | data class UserBean(val userName: String = "dark", val userAge: Int) |
它的输出结果如下
1 | UserBean(userName=null, userAge=26) |
这里直接放结论,因为 Gson
使用了 UnsafeAllocator
没有使用任何构造函数,所以默认值并不会被加进去,而是会出现 null
解决 gson 反序列化中 null 的方法
为什么我们要在之前分析 gson
的生成 ObjectConstructor
的多种方式呢?
最重要的点就在于可以帮我们找到解决问题的思路,也就是看看如何不使用 newUnsafeAllocator
生成无参构造函数
添加无参构造函数
1
2
3data class UserBean(val userName: String, val userAge: Int){
constructor():this(userName = "dark", userAge = 0)
}使用 @JvmOverloads
这个方法类似于第一种,也是相当于提供了一个无参构造函数1
data class UserBean constructor(val userName: String = "dark", val userAge: Int = 0)
注意
@JvmOverloads 会将有默认值的参数依次忽略,生成重载方法,所以我们需要将所有参数都给上默认值,这样才能生成默认构造函数
不使用
data class
像 java 一样,写成成员变量(字段)1
2
3
4
5
6
7
8
9
10class UserBean {
var userName = "dark"
var userAge = 0
override fun toString(): String {
return "UserBean(userName=$userName, userAge=$userAge)"
}
}
这前三种都是通过生成默认构造函数
这里我们继续打断点 debug 看出,gson
调用的是 newDefaultConstructor
终极方案 放弃 Gson
比如我们可以改用 moshi
gson 目前对 kotlin 还不够友好,moshi 对 kotlin 的支持会更好,里面有专门针对 kotlin 的适配器
moshi 是 square 公司提供的一个开源库,就是那个开发了 okhttp 的公司
1 | import com.squareup.moshi.JsonAdapter |
1 | UserBean(userName=dark, userAge=26) |
另外,还有之前的问题,moshi 并不会在出现类型冲突的时候,成功反序列化
1 |
|
1 | Exception in thread "main" com.squareup.moshi.JsonDataException: Non-null value 'userName' was null at $.userName |
四、扩展知识(平台类型)
再来看个扩展知识,和 Gson 无直接关联,但是在开发中也是蛮重要的一个知识点
json 为空字符串,此时 Gson 可以成功反序列化,且得到的 userBean 为 null
还是以 data class UserBean(val userName: String, val userAge: Int)
为例子
1 | fun main() { |
如果加上类型声明:UserBean?,那也可以成功反序列化
1 | fun main() { |
如果加上的类型声明是 UserBean 的话,那就比较好玩了,会直接抛出 NullPointerException
1 | fun main() { |
1 | Exception in thread "main" java.lang.NullPointerException: fromJson(...) must not be null |
以上三个例子会有不同区别的原因是什么呢?
这是在 kotlin 调用 java 的时候会出现的,官方文档写着“Java 中的任何引用都可能是 null
,这使得 Kotlin 对来自 Java 的对象要求严格空安全是不现实的。 Java 声明的类型在 Kotlin 中会以特殊方式对待并称为平台类型。”
就是 kotlin 在调用 java 代码的时候,会把 java 里的类型以平台类型(platform type)对待,例如 java 中的 String 会当做 String!
平台类型只是编译器的中间产物,并不能显式使用在代码里, val s:String!
是不允许出现在代码里的
所以对于写 kotlin 的程序员来说,在使用 kotlin 调用 java 代码的时候,稳妥的方式是自己进行判空处理,或者对于 java 程序员来说,写上 @NotNull 和 @Nullable 这类参数
回到我们的这个,当我们从 Kotlin
承接 Gson
这个 Java
类返回的变量时,既可以将其当做 UserBean
类型,也可以当做 UserBean?
类型。而如果我们直接显式声明为 UserBean
类型,就说明我们确信返回的是非空类型,当返回的是 null
时就会触发 Kotlin
的 null check
,导致直接抛出 NullPointerException
- Title: Kotlin data class 被 gson 解析遇到的坑
- Author: lucas
- Created at : 2025-02-27 15:20:51
- Updated at : 2025-03-02 21:19:15
- Link: https://darkflamemasterdev.github.io/2025/02/27/Kotlin-data-class-被-gson-解析遇到的坑/
- License: This work is licensed under CC BY-NC-SA 4.0.