Gson与Kotlin"摩擦"的那件小事

8,393 阅读4分钟

大家好,本篇文章分享一下之前使用gson和kotlin碰撞出的一些火花,脑瓜子被整的懵懵的那种。

准备知识

总所周知,当Gson没有无参构造函数时,会使用UnSafe以一种非安全的方式去创建类的对象,这样会产生两个问题:

  1. 属性的默认初始值会丢失,比如某个类中有这么一个属性public int age = 100,经过unsafe创建该类对象,会导致age的默认值100丢失,变为0;
  1. 会绕过Kotlin的空安全检验,因为经过unsafe创建的对象不会在属性赋值时进行可null校验。

所以一般比较在使用Gson反序列化时,比较推荐的做法就是反序列化的类要有无参构造函数。

PS:其实提供了无参构造函数,还是有可能会绕过Kotlin空安全校验,毕竟在Gson中属性是通过反射赋值的,所以一些人会推荐使用Moshi,这个笔者还没怎么使用过,后续会了解下。

看一个脑瓜子懵的例子

先上代码:

class OutClass {
    val age: Int = 555

    override fun toString(): String {
        return "OutClass[age = $age]"
    }

    inner class InnerClass {
        val age1: Int = 897

        override fun toString(): String {
            return "InnerClass[age = ${this.age1}]"
        }

    }
}

以上两个类OutClassInnerClass看起来都有无参构造函数,现在我们来对其进行一一反序列化。

1. 反序列化OutClass

fun main(args: Array<String>) {
    val content = "{"content": 10}"
    val out = OutClass::class.java
    val obj = Gson().fromJson(content, out)
    println(obj)
}

反序列化使用的字符串是一个OutClass类不存在的属性content,咱们看下输出结果:

看起来没毛病,由于存在无参构造函数,且反序列化所使用的字符串也不包括age字段,age的默认值555得以保留。

2. 反序列化InnerClass

先上测试代码:

fun main(args: Array<String>) {
    val content = "{"content": 10, "location": null}"
    val out = OutClass.InnerClass::class.java
    val obj = Gson().fromJson(content, out)
    println(obj)
}

运行结果如下:

不是InnerClass也是有无参构造函数的吗,为啥age字段的默认值897没有被保留,当时给整蒙了。

于是进行了下debug断点调试,发现最终是通过Unsafe创建了InnerClass

当时是百思不得其解,后续想了想,非静态内部类本身会持有外部类的引用,而这个外部类的引用是通过内部类的构造方法传入进来的,咱们看一眼字节码:

所以非静态内部类根本就没有无参构造方法,所以最终通过Gson反序列化时自然就是通过Unsafe创建InnerClass对象了。

如果想要解决上面这个问题,将非静态内部类改成静态内部类就行了,或者尽量避免使用非静态内部类作为Gson反序列化的类。

另外大家如果感兴趣想要了解下Gson是如何判断的反射无参构造方法还是走Unsafe创建对象的,可以看下源码:

ReflectiveTypeAdapterFactory#create ——>ConstructorConstructor#get

介绍下typeOf()方法

回忆下我们之前是怎么反序列化集合的,看下下面代码:

fun main(args: Array<String>) {
    val content = "[{"content": 10, "location": "aa"}, {"content": 10, "location": "bb"}]"
    val obj = Gson().fromJson<List<OutClass>>(content, object : TypeToken<List<OutClass>>(){}.type)
    println(obj)
}

要创建一个很麻烦的TypeToken对象,获取其type然后再进行反序列化,输出如下正确结果:

为了避免麻烦的创建TypeToken,我之前写了一篇文章来优化这点,大家感兴趣的可以看下这篇文章:Gson序列化的TypeToken写起来太麻烦?优化它

然后之前有个掘友评论了另一个官方提供的解决方法:

于是我赶紧试了下:

@OptIn(ExperimentalStdlibApi::class)
fun main(args: Array<String>) {
    val content = "[{"content": 10, "location": "aa"}, {"content": 10, "location": "bb"}]"
    val obj = Gson().fromJson<List<OutClass>>(content, typeOf<List<OutClass>>().javaType)
    println(obj)
}

运行输出:

没毛病,这个写法要比创建一个TypeToken简单多了,这个api是很早就有了,不过到了kotlin1.6.0插件版本才稳定的,请大家注意下这点:

十分推荐大家使用这种方式,官方支持,就突出一个字:稳。

总结

本篇文章主要是给大家介绍了Gson反序列化非静态内部类时的坑,以及介绍了一个官方支持的api:typeOf(),帮助大家简化反序列化集合的操作,希望本篇文章能对比有所帮助。

历史文章

两个Kotlin优化小技巧,你绝对用的上

Kotlin1.9.0-Beta,它来了!!

Kotlin1.8新增特性,进来了解一下

聊聊Kotlin1.7.0版本提供的一些特性

聊聊kotlin1.5和1.6版本提供的一些新特性

kotlin密封sealed class/interface的迭代之旅

优化@BuilderInference注解,Kotlin高版本下了这些“毒手”!

@JvmDefaultWithCompatibility优化小技巧,了解一下~