Gson如何解决可变类型集合(Map/List)序列化

Gson的序列化功能

  • 支持int, long, bool, string等基本类型
  • 支持Url, 数组, atomic, Bean等各种类型
  • List, Map类型也支持泛型
  • 支持数值Number的转换规则
  • 支持属性命名下划线/驼峰
  • 支持过滤某写属性
  • 支持自定义序列化adapter

遇到的问题

尽管gson足够强大, 但是还是遇到了问题

譬如:

Map<String, Object> map = new HashMap<>();
map.put("int", 22);
map.put("long", 2211L);
float flo = 333.666f;
map.put("float", flo);
double dou = 122.55f;
map.put("double", dou);
map.put("bean", new TestBean("desc"));
String json = new Gson.toJson(map);
Map<String, Object> map2 = new Gson.fromJson(json, HashMap.class);

经过序列化/反序列化,返回的map2, 如下效果:


16433549481788.jpg

可见, int, long, float全都转成double; 而bean类被转成LinkTreeMap

同理, list也是一样的问题;

解决

可不可通过自定义gson序列化解决? 答案是可以的

通过自定义Gson gson = new GsonBuilder(), 然后设置
.registerTypeHierarchyAdapter(Map.class, new TypeAdapter<Map<String, Object>>()

.registerTypeHierarchyAdapter(Map.class, new TypeAdapter<Collection<String, Object>>()
即可

基本原理

定制gson的typeAdapter, 写入时在数据头部插入map/list的自身和item的class字典信息, 然后解析的时候解析字典, 反序列化正确的类型.

代码如下

/**
 * Created by Supylc on 2022/1/25.
 * 自定义gson,功能为:
 * 1、能解析可变类型的Map, 例如Map<String, Object>
 * 2、能正确解析Map里面的float,double,long,int,short,byte,char等
 * 3、需要用GsonCasual类定制的Gson才能正确解析,如果在本类写入map,又用外部gson读,则维持原解析效果
 *
 * 适用场景:
 * 需要用到map或list序列化的地方(特别是item为可变类型)
 *
 * 注意:
 * 用GsonCasual写和读,不要出现用GsonCasual写然后用其他Gson读(反之亦然)读情况
 */
public class GsonCasual {

    private static final String TAG = "GsonCasual";
    private static final String DICT = "__clz_dict__";
    private static final Map<String, Class<?>> mClazzCacheMap = new HashMap<>();

    /**
     * 设置支持map和list的准确解析,
     * 原理:
     * 1、自定义读写map和list
     * 2、在写json的头部,插入class字典,包含Map(或List)和key-value(或list-item)的类型信息
     *
     * 字典格式为:
     * 数组大小,map(list)的类型class,value(map或list的item)的类型class
     * 如:5,java.util.HashMap, java.lang.long, java.lang.Integer, java.lang.String
     *
     * 把字典信息独立放在头部,对旧的map数据解析不做修改,容错性更好(比如不知情的情况下,用不同的gson进行读写,也不报错)
     */
    private static final Gson gson = new GsonBuilder()
            //处理所有的map类型
            .registerTypeHierarchyAdapter(Map.class, new TypeAdapter<Map<String, Object>>() {

                @Override
                public void write(JsonWriter out, Map<String, Object> value) throws IOException {
                    if (value == null) {
                        out.nullValue();
                        return;
                    }
                    out.beginObject();
                    Set<Map.Entry<String, Object>> entrySet = value.entrySet();
                    Object entryValue;
                    //在头部写一个class字典数据
                    writeClassDict(out, value);
                    for (Map.Entry<String, Object> entry: entrySet) {
                        entryValue = entry.getValue();
                        if (entryValue == null) {
                            out.name(entry.getKey()).nullValue();
                        } else {
                            out.name(entry.getKey()).jsonValue(gson.toJson(entryValue));
                        }
                    }
                    out.endObject();
                }

                @Override
                public Map<String, Object> read(JsonReader in) throws IOException {
                    JsonToken jsonToken = in.peek();
                    if (jsonToken == JsonToken.NULL) {
                        return null;
                    }
                    Map<String, Object> result = null;
                    jsonToken = in.peek();
                    if (jsonToken != JsonToken.BEGIN_OBJECT) {
                        throw new NullPointerException("firstToken is wrong, gson invalid?");
                    }
                    in.beginObject();
                    int kvCount = 0;
                    String[] valueClazzArray = null;
                    while (in.hasNext()) {
                        String name = in.nextName();
                        Object readValue;
                        if (DICT.equals(name)) {
                            valueClazzArray = readDictObject(in);
                            continue;
                        }
                        if (result == null && valueClazzArray != null) {
                            result = (Map<String, Object>) newInstance(valueClazzArray[0]);
                        }
                        if (valueClazzArray != null) {
                            String valueClazz = valueClazzArray[kvCount + 1];
                            if (valueClazz == null) {
                                readValue = null;
                            } else {
                                readValue = gson.fromJson(in, getClazz(valueClazz));
                            }
                            kvCount ++;
                        } else {
                            readValue = gson.fromJson(in, String.class);
                        }

                        if (result == null) {
                            result = new HashMap<>();
                        }
                        result.put(name, readValue);
                    }
                    in.endObject();
                    if (result == null) {
                        result = new HashMap<>();
                    }
                    return result;
                }
            })
            //处理所有的集合类型
            .registerTypeHierarchyAdapter(Collection.class, new TypeAdapter<Collection<?>>() {

                @Override
                public void write(JsonWriter out, Collection<?> value) throws IOException {
                    if (value == null) {
                        out.nullValue();
                        return;
                    }
                    out.beginArray();
                    //在头部写一个class字典数据
                    writeClassDict(out, value);
                    for (Object entry: value) {
                        if (entry == null) {
                            out.nullValue();
                        } else {
                            out.jsonValue(gson.toJson(entry));
                        }
                    }
                    out.endArray();
                }

                @Override
                public Collection<?> read(JsonReader in) throws IOException {
                    JsonToken jsonToken = in.peek();
                    if (jsonToken == JsonToken.NULL) {
                        return null;
                    }
                    Collection<Object> result = null;

                    int kvCount = 0;
                    String[] valueClazzArray = null;
                    in.beginArray();
                    jsonToken = in.peek();
                    if (jsonToken == JsonToken.BEGIN_ARRAY) {
                        valueClazzArray = readDictObject(in);
                    }
                    while (in.hasNext()) {
                        Object readValue;
                        if (result == null && valueClazzArray != null) {
                            result = (Collection<Object>) newInstance(valueClazzArray[0]);
                        }
                        if (valueClazzArray != null) {
                            String valueClazz = valueClazzArray[kvCount + 1];
                            if (valueClazz == null) {
                                readValue = null;
                            } else {
                                readValue = gson.fromJson(in, getClazz(valueClazz));
                            }
                            kvCount ++;
                        } else {
                            readValue = gson.fromJson(in, String.class);
                        }

                        if (result == null) {
                            result = new ArrayList<>();
                        }
                        result.add(readValue);
                    }
                    in.endArray();
                    return result;
                }
            })
            .create();

    private static void writeClassDict(JsonWriter out, Object value) throws IOException {
        if (value instanceof Map) {
            Set<Map.Entry<String, Object>> entrySet = ((Map<String, Object>) value).entrySet();
            out.name(DICT);
            out.beginArray();
            out.value(entrySet.size() + 1);
            out.value(value.getClass().getName());
            for (Map.Entry<String, Object> entry: entrySet) {
                Object entryValue = entry.getValue();
                if (entryValue == null) {
                    out.nullValue();
                } else {
                    out.value(entryValue.getClass().getName());
                }
            }
            out.endArray();
        } else if (value instanceof Collection) {
            Collection<?> collection = (Collection<?>) value;
            out.beginArray();
            out.value(collection.size() + 1);
            out.value(value.getClass().getName());
            for (Object entry: collection) {
                if (entry == null) {
                    out.nullValue();
                } else {
                    out.value(entry.getClass().getName());
                }
            }
            out.endArray();
        }
    }

    private static String[] readDictObject(JsonReader in) throws IOException {
        int dictCount = 0;
        String[] valueClazzArray = null; //包含root类型以及item类型
        JsonToken jsonToken;
        in.beginArray();
        while (in.hasNext()) {
            if (dictCount > 1) {
                jsonToken = in.peek();
                if (jsonToken == JsonToken.NULL) {
                    in.nextNull();
                } else {
                    valueClazzArray[dictCount - 1] = in.nextString();
                }
            } else if (dictCount == 1) {
                valueClazzArray[0] = in.nextString();
            } else if (dictCount == 0) {
                valueClazzArray = new String[in.nextInt()];
            }
            dictCount++;
        }
        in.endArray();
        return valueClazzArray;
    }

    public static String toJson(Object src) {
        return gson.toJson(src);
    }

    public static <T> T fromJson(String json, Class<T> clazz) {
        return gson.fromJson(json, clazz);
    }

    private static Class<?> getClazz(String clazzName) {
        Class<?> clazz = mClazzCacheMap.get(clazzName);
        if (clazz == null) {
            try {
                clazz = Class.forName(clazzName);
            } catch (Exception e) {
                log("getClazz, not found class: " + clazzName);
                e.printStackTrace();
            }
        }
        return clazz;
    }

    private static Object newInstance(String clazz) {
        try {
            return getClazz(clazz).newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private static void log(String log) {
        Log.i(TAG, log);
    }

用例

只需要用 GsonCasual.toJson/fromJson 即可, 使用也很简单

总结

  1. 原想用parcel方式解决, 因为parcel序列化更直接,更节省空间
    用记录class字典的方式, parcel也能实现, 但最后写出来, 其实也是走gson走过的路, 因此何必重复造轮子? 明显的, parcel不适合用在此场景

  2. 选择gson的自定义实现, 效率也很高, 实现成本更低

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

推荐阅读更多精彩内容