Gson---手把手教你官方json转换框架的用法以及使用过程中的坑范型类型擦除问题

虽然在网络请求中Retrofit已经通过converter-gson帮我们完美地处理了json转换的问题,但是在实际开发中还是难免会遇到需要手动进行json转换的情况。这时就需要用到谷歌官方的json转换框架---Gson。其实converter-gson也是基于Gson来完成的。
Gson其实已经被封装的非常完美了,但在在使用过程中还是有许多需要注意的地方。下面,从头开始讲解Gson的使用,相信我,花20分钟看完这篇文章,你将完全掌握Gson的各种使用姿势。

一.引入

Gson有时候被其他的包所使用,直接包含在其他依赖里,比如Retrofit的converter-gson中。但是也可以单独引入Gson:

implementation 'com.google.code.gson:gson:2.8.6'

二.基本用法

2.1 实体类转json

这个没什么好说的,只需要一句话即可:

 Gson().toJson(entity)
2.2 json转实体类

其实也只需要一个方法

Gson().fromJson("<json>",Entity::class.java)

这看起来非常简单,但是其实其中也包含着坑,打个比方,如果这里的Entity是BaseEntity<T>呢,这里边包含着范型。

那Json还能转换吗?(稍后详细说明)

2.3 Android Studio生成实体类

现在有如下Json数据:

{
    "code":0,
    "message":"",
    "content":{
        "city":"cheng du",
        "weather":"sunny",
        "tem":16,
        "date":"2020-10-19"
    }
}

是一个简单的天气数据,其中code和message是服务器返回的状态码和异常信息,code=0时表示请求正常。其她的字段意义一看就懂。

要使用这个Json数据,我们首先要创建实体类,还好Android Studio已经有插件可以一键生成了,在插件中搜索“json”

image-20201019111014650.png
image-20201019175258531.png

现在我的项目全部使用Kotlin ,所以就只安装第一个插件就够了,用法如下:先随便创建一个Kotlin的.kt文件,例如我这里是TestEntity,然后 右键--->Generate--->Kotlin data class from JSON

创建内容如下的实体类(注意手动添加Serializable接口)

import java.io.Serializable

data class TestEntity(
    val code: Int,
    val content: Content,
    val message: String
) : Serializable

data class Content(
    val city: String,
    val date: String,
    val tem: Int,
    val weather: String
) : Serializable
2.4 实际操作进行一下转换

我在MainActivity中加入如下方法:

    val TAG="JsonTest"
    private fun start() {
        val json="{\n" +
                "    \"code\":0,\n" +
                "    \"message\":\"\",\n" +
                "    \"content\":{\n" +
                "        \"city\":\"cheng du\",\n" +
                "        \"weather\":\"sunny\",\n" +
                "        \"tem\":16,\n" +
                "        \"date\":\"2020-10-19\"\n" +
                "    }\n" +
                "}"
        try {
            //json转实体类
            val entity= Gson().fromJson(json,TestEntity::class.java)
            Log.d(TAG,"json转换为实体类---完成")
            Log.d(TAG,"城市:${entity.content.city}")
            Log.d(TAG,"天气:${entity.content.weather}")
            Log.d(TAG,"气温:${entity.content.tem}")
            Log.d(TAG,"时间:${entity.content.date}")

            //实体类转json
            val json2=Gson().toJson(entity)
            Log.d(TAG,"实体类转换成json---完成")
            Log.d(TAG,json2)
        }catch (e:Exception){
            Log.d(TAG,"json格式错误")
        }
    }

这个方法执行结果

image-20201019175258531.png

三. 进阶用法---处理实体类中的范型

通常,在网络请求中,各个接口虽然内容不一样,但是最外层都有统一的格式,例如上文json数据的外层中有code和message。这是服务器对所有接口请求结果是否正常的统一返回,所以通常在开发中,我们需要统一地来处理最外层的数据,这就需要使用到范型对实体类进行包装。

还是基于之前的json数据,首先创建一个实体类 BaseEntity,其内容如下:

import java.io.Serializable
data class BaseEntity<T>(
    val code: Int = 0,
    val message: String = "",
    val content: T?
) : Serializable

这个实体类是所有接口返回数据的一个外层包装,然后使用范型代表content中的不同数据。通常我是会给实体类中不为空的数据一个默认的值,尽量避免空指针,当然这也只是看个人习惯,觉得不优雅,可以不加。
然后再为层的json数据创建实体WeatherEntity

import java.io.Serializable
class WeatherEntity(
    val city: String = "",
    val date: String = "",
    val tem: Int = 0,
    val weather: String = ""
) : Serializable

这样,我们的完整实体类就从之前的TestEntity变成了---->BaseEntity<WeatherEntity>
那如果还是使用之前的方式来进行json转换是否可行呢?尝试一下修改之前start方法:

编译不通过

可以看到,在数据实体类中包含了范型的情况下,Kotlin编译不通过。
这时,就需要采用另外一种方法 向fromJson中传入实体类的Type来进行json转换。
将start方法中的转换的代码修改为如下:

            val type = object : TypeToken<BaseEntity<WeatherEntity>>() {}.type
            val entity: BaseEntity<WeatherEntity>? = Gson().fromJson<BaseEntity<WeatherEntity>>(json, type)

这样就可以了

四. 进阶用法---遇到的范型类型擦除问题和解决方式

4.1 背景介绍

新建一个抽象类HttpCallback,代码在下方。
看这个抽象类HttpCallback的代码,是不是非常眼熟呢,如果经常使用Rxjava+Retrofit的方式来进行网络请求的话,对这个代码一定不会陌生

/**
 * 网络请求回调
 * */
abstract class HttpCallback<T> {

    //获取到数据
    fun onNext(data: String) {


    }

    //请求成功
    abstract fun onSuccess(entity: T)

    //服务器响应异常
    open fun onResponseError(code: Int, message: String) {
        //统一错误处理

    }

    //请求错误
    fun onError(throwable: Throwable) {

    }

    //网络错误
    open fun onHttpError(code: Int, message: String) {
        //统一错误处理

    }
}

简单介绍一下,在这个回调中,我们在onNext拿到服务器返回的数据进行处理和json的转换,将json数据转换成范型对应的实体对象,然后在onSuccess中传出这个对象。当然中间还有许多错误和空值的判断和处理。
MainActivity中设置回调后,就只需要关心onSuccess中的实体对象,和其他的错误信息了,非常的方便。
我们现在修改start方法如下:

    private fun start() {
        //设置匿名类回调对象
        val callBack = object : HttpCallback<WeatherEntity>() {
            override fun onSuccess(entity: WeatherEntity) {
                Log.d(TAG, "json转换为实体类---完成")
                Log.d(TAG, "城市:${entity.city}")
                Log.d(TAG, "天气:${entity.weather}")
                Log.d(TAG, "气温:${entity.tem}")
                Log.d(TAG, "时间:${entity.date}")
            }

            override fun onResponseError(code: Int, message: String) {
                super.onResponseError(code, message)
            }

            override fun onHttpError(code: Int, message: String) {
                super.onHttpError(code, message)
            }
        }

        //模拟数据
        val data = "{\n" +
                "    \"code\":0,\n" +
                "    \"message\":\"\",\n" +
                "    \"content\":{\n" +
                "        \"city\":\"cheng du\",\n" +
                "        \"weather\":\"sunny\",\n" +
                "        \"tem\":16,\n" +
                "        \"date\":\"2020-10-19\"\n" +
                "    }\n" +
                "}"

        //模拟请求成功
        callBack.onNext(data)
    }
4.2 问题介绍

在上边的代码中,在Mactivity中用起来确实很方便了,有一个统一的地方来进行json转换和统一错误处理了。只需要将范型传入,在onSuccess中处理结果就行了。在是现在问题来了,我们怎么在HttpCallback中把json转为范型 T 对应的实体对象呢?

如果再使用之前的方法来进行转换可以吗?修改onNext代码如下:

    //获取到数据
    fun onNext(data: String) {
        val json = data
        val type = object : TypeToken<BaseEntity<T>>() {}.type
        val bean = Gson().fromJson<BaseEntity<T>>(json, type)
        if (bean.content != null) {
            onSuccess(bean.content)
        } else {
            onResponseError(-1, "没有数据")
        }
    }

运行一下,程序崩溃,Gson报错:

java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to WeatherEntity

因为HttpCallback这个抽象类中的范型类型被擦除了,Gson不知道该该将数据转换成什么对象。
这时强行转为WeatherEntity则崩溃。
下面我将介绍两个解决方式,初级的是我最开始写的,高级是在请教公司的师兄后,他给出的一个方案。

4.3 初级解决方法

首先我们来看Gson源码中Gson().fromJson这个json转换成实体类对象的核心方法,重点关注以下两个重写:
通过type进行转换

  public <T> T fromJson(String json, Type typeOfT) throws JsonSyntaxException {
    if (json == null) {
      return null;
    }
    StringReader reader = new StringReader(json);
    T target = (T) fromJson(reader, typeOfT);
    return target;
  }

通过Class进行转换

  public <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException {
    Object object = fromJson(json, (Type) classOfT);
    return Primitives.wrap(classOfT).cast(object);
  }

下面来看一下我的onNext代码:

    fun onNext(data: String) {
        //如果data需要解密 则通过解密来获得json数据
        val json = data
        try{
            //第一次转换获取BaseEntity
            val type = object : TypeToken<BaseEntity<Any>>() {}.type
            //通过type进行转换
            val baseBean = Gson().fromJson<BaseEntity<Any>>(json, type)
            Log.d(TAG, "json第一次转换获取BaseEntity---完成 code:${baseBean.code} message:${baseBean.message}")
            //第二次转换获取content
            //获取泛型的Class
            val classT =
                (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0] as Class<T>
            //获取第一次转换后的content的json字符串
            val contentJson = Gson().toJson(baseBean.content)
            //通过class进行转换
            val bean = Gson().fromJson(contentJson, classT)
            Log.d(TAG, "json第一次转换获取content--完成")
            onSuccess(bean)
            
        }catch (e:Exception){
            onResponseError(-1, "Json格式错误")
        }
    }

注释已经很清晰了,但还是简单说一下思路:
首先获取到json后,因为网络请求中所有的数据都通过BaseEntity进行包装,所以:
第一步
将json转换为BaseEntity<Any>对象baseBean,获取到code和message进行一些异常判断
并且将baseBean.content通过Gson().toJson(baseBean.content)转换为content对应的json字符串contentJson
第二步
获取T对应的Class,通过fromJson进行二次转换,最终获取到我们想要的实体类对象,传递到onSuccess

            //获取泛型的Class
            val classT =
                (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0] as Class<T>
            //获取第一次转换后的content的json字符串
            val contentJson = Gson().toJson(baseBean.content)
            //通过class进行转换
            val bean = Gson().fromJson(contentJson, classT)

当然,这只是最简写法,中间还有一些code的判断,非空判断,都需要报相应的异常。

这种写法虽然可以解决范型类型擦除的问题了,但是可以看到,在整个过程中进行了两次将json转换成对象,一次将对象转换成json。如果每个接口每次请求都要这样处理,那么将会造成内存的浪费,所以,接下来看看公司师兄的方法,一次转换就搞定。

4.4 高级解决方法

HttpCallback完整代码:

/**
 * 网络请求回调
 * */
abstract class HttpCallback<T> {
    val TAG = "JsonTest"

    //获取到数据
    fun onNext(data: String) {
        //如果data需要解密 则通过解密来获得json数据
        val json = data

        val classT =
            (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0] as Class<T>

        val jsonType = ResultParameterType(classT)

        val baseBean = try {
            Gson().fromJson<BaseEntity<T>>(json, jsonType)
        } catch (e: Exception) {
            null
        }

        if(baseBean?.content != null && baseBean.code == 0){
                onSuccess(baseBean.content)
        }
    }

    //请求成功
    abstract fun onSuccess(entity: T)

    //服务器响应异常
    open fun onResponseError(code: Int, message: String) {
        //统一错误处理

    }

    //请求错误
    fun onError(throwable: Throwable) {

    }

    //网络错误
    open fun onHttpError(code: Int, message: String) {
        //统一错误处理

    }

    class ResultParameterType(private val type: Type) : ParameterizedType {
        override fun getRawType(): Type {
            return BaseEntity::class.java
        }

        override fun getOwnerType(): Type? {
            return null
        }

        override fun getActualTypeArguments(): Array<Type> {
            return arrayOf(type)
        }
    }
}

可以看到只转换了一次就搞定了 ,完美。

然而我并没有完全理解这个代码,正在学习中,师兄和我讲的是,得搞清楚这个Gson().fromJson方法需要的参数到底是什么,可以是Class,也可以是Type。只需要准备好它需要的参数传递给它就好了,这个参数可以从BaseEntity<T>得到,也可以从T 得到,只是最后的效果不同。

在开发过程中这种思想很重要,不是你觉得某个方法要什么参数,而是得看它真正需要的是什么,从而找到最合适的参数进行传递。同样的,一个问题不是解决了就ok了,还需要知道原理从而找到最合适的解法,这样写出来的代码才会优雅。

总之学习之路漫漫,共勉。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345