如何保持json序列化的順序性?

等你歸去來發表於2021-01-10

  說到json,相信沒有人會陌生,我們天天都在用。那麼,我們來討論個問題,json有序嗎?是誰來決定的呢?如何保持?

  說到底,json是框架還是啥?實際上它只是一個資料格式,一個規範標準,它永遠不會限制實現方的任何操作,即不會自行去保證什麼順序性之類的。json的格式僅由寫入資料的一方決定其長像如何。而資料讀取一方,則按照json的協議標準進行解析,即可理解原資料的含義。json擁有較為豐富的資料格式,所以對當前應用還是比較友好的。

  那麼,我們如何處理json的順序性呢?

 

1. 保持json有序的思路

  首先,我們要澄清有序性的概念:從某種程度上,我們可以把json看作是一個個的kv組成的資料,從這個層面上來講,我們可以把有序性定義為json的key保持有序,先假設為字典序吧,那麼就說這個json資料是有序的。

  其次,因為json的資料支援巢狀,所以,我們應該需要保持每一層的資料都有序,才是完整有序的。

  ok, 理解完有序的概念,下面我們來看看如何實現有序?

  json本身是不可能保持有序了,所以,當我們自行寫入json資料時,只需要按照 abcde... 這種key順序寫入資料,那麼得到的最終json就是有序的。

  但我們一般都是使用物件進行程式變換的,所以,就應該要從物件中取出有序的key, 然後序列化為json.

  這裡保持有序,至少有兩個層面的有序:1. kv形式的key的有序; 2. 列表形式的資料有序; 還有其他可能非常複雜的有序性需求,比如按照某欄位有序,倒序。。。

  所以,想保持json有序很簡單,保證有序寫入就可以了。(貌似等於沒有說哦)

 

2. 保持json有序的應用場景舉例

  為什麼要保持json有序呢?json相當於kv資料,一般情況下我們是不需要保證有序的,但有些特殊情況下也許有用。比如我有兩份json資料,我想比較它們是否是相等的時候!

  比如第一份資料是 {"a":1, "b":2}, 第二份資料是 {"b":2, "a":1}, 那麼你說這兩份資料是否是相等的呢?相等或不相等依據是啥?

  如果對於固定資料結構的json, 那麼也許我們可以直接取出每個key的值,然後進行比較,全部相等則相等成立,否則不相等。

  但對於json本身就是各種不確定的資料組成,如果我們限制死必須取某些key, 那麼這個通用性就很差了。所以,我們要想比較兩個json是否相等,還應該要有另外的依據。

  另外,當我們將有序json寫入檔案之後,當key的資料非常多時,有序實際上可以輔助我們快速找到對應的key所在的位置。這是有序性帶來的好處,快速查詢!

  比如下面的例子,對比兩個結果集是否相等,你覺得結果當如何呢?

    @Test
    public void testJsonObjectOrder() {
        String res1, res2;
        List<Map<String, Object>> nList;
        Map<String, Object> data = new HashMap<>();
        data.put("d", "cd");
        data.put("a", 1);
        data.put("b", 0.45);
        data.put("total", 333);
        List<Map<String, Object>> list = new ArrayList<>();
        Map<String, Object> item1 = new HashMap<>();
        item1.put("aa", 1);
        item1.put("ee", 5);
        item1.put("bb", 6);
        item1.put("nn", null);
        list.add(item1);
        Map<String, Object> item2 = new HashMap<>();
        item2.put("xxx", "000");
        item2.put("q", 2);
        item2.put("a", "aa");
        list.add(item2);
        data.put("sub", list);


        Map<String, Object> nData = new HashMap<>(data);
        nData.put("c", null);
        nData.put("abc", null);

        res1 = JSONObject.toJSONString(data);
        res2 = JSONObject.parseObject(JSONObject.toJSONString(nData)).toJSONString();
        Assert.assertEquals("序列化結果不相等default", res1, res2);

        res2 = JSONObject.toJSONString(JSONObject.parseObject(
                JSONObject.toJSONString(nData, SerializerFeature.SortField)),
                SerializerFeature.SortField);
        Assert.assertEquals("序列化結果不相等sort", res1, res2);

        nList = new ArrayList<>();
        nList.add(item2);
        nList.add(item1);
        nData.put("sub", nList);
        res2 = JSONObject.parseObject(JSONObject.toJSONString(nData)).toJSONString();
        Assert.assertEquals("序列化結果不相等array", res1, res2);

    }

  以上是fastjson庫進行json序列化的處理方式,json的資料結構大部分使用可以用map進行等價,除了純陣列的結構以外。以上測試中,除了最後一個array的位置調換,導致的結果不一樣之外,總體還是相等的。糾其原因,是因為原始資料結構是一致的,而fastjson從一定程度上維持了這個有序性。

  

3. fastjson維護json的有序性的實現

  很顯然,讓我們自行寫json的工具類,還是有一定的難度的,至少要想高效完整地寫json是困難的。所以,一般我們都是藉助一些現有的開源類庫。

  上一節中說到,fastjson維護了json一定的順序性,但是並非完整維護了順序性,它的順序性要體現在,相同的資料結構序列化的json,總能得到相同的反向的相同資料結構的資料。比如,ArrayList 的順序性被維護,map的順序性被維護。

  但是很明顯,這些順序性是根據資料結構的特性而定的,而非所謂的字典序,那麼,如果我們想維護一個保持字典序的json如何處理呢?看看下面的實現:

public class JsonObjectTest {

    @Test
    public void testJsonObjectOrder() {
        String res1, res2;
        List<Map<String, Object>> nList;
        Map<String, Object> data = new HashMap<>();
        data.put("d", "cd");
        data.put("a", 1);
        data.put("b", 0.45);
        data.put("total", 333);
        List<Map<String, Object>> list = new ArrayList<>();
        Map<String, Object> item1 = new HashMap<>();
        item1.put("aa", 1);
        item1.put("ee", 5);
        item1.put("bb", 6);
        item1.put("nn", null);
        list.add(item1);
        Map<String, Object> item2 = new HashMap<>();
        item2.put("xxx", "000");
        item2.put("q", 2);
        item2.put("a", "aa");
        list.add(item2);
        data.put("sub", list);


        Map<String, Object> nData = new HashMap<>(data);
        nData.put("c", null);
        nData.put("abc", null);

        res1 = JSONObject.toJSONString(data);
        res2 = JSONObject.parseObject(JSONObject.toJSONString(nData)).toJSONString();
        Assert.assertEquals("序列化結果不相等default", res1, res2);

        res2 = JSONObject.toJSONString(JSONObject.parseObject(
                JSONObject.toJSONString(nData, SerializerFeature.SortField)),
                SerializerFeature.SortField);
        Assert.assertEquals("序列化結果不相等sort", res1, res2);

        res2 = JSONObject.toJSONString(JSONObject.parseObject(
                JSONObject.toJSONString(nData, SerializerFeature.WriteMapNullValue)),
                SerializerFeature.WriteMapNullValue);
        Assert.assertEquals("序列化結果不相等null", res1, res2);

        nList = new ArrayList<>();
        nList.add(item2);
        nList.add(item1);
        nData.put("sub", nList);
        res2 = JSONObject.parseObject(JSONObject.toJSONString(nData)).toJSONString();
        Assert.assertEquals("序列化結果不相等array", res1, res2);

        nList = new ArrayList<>();
        nList.add(item2);
        nList.add(item1);
        nData.put("sub", nList);
        res1 = transformDataToJSONAsOrderWay(data);
        res2 = transformDataToJSONAsOrderWay(nData);
        Assert.assertEquals("序列化結果不相等array-s", res1, res2);



    }

    /**
     * 將原始資料轉換為有序的集合
     */
    private String transformDataToJSONAsOrderWay(Map<String, Object> data) {
        TreeMap<String, Object> transformedData = new TreeMap<>();
        for (Map.Entry<String, Object> entry : data.entrySet()) {
            if(entry.getValue() == null) {
                continue;
            }
            if(entry.getValue() instanceof List) {
                TreeMap<String, Integer> tmpMap = new TreeMap<>();
                List value = (List) entry.getValue();
                for (int i = 0; i < (value).size(); i++) {
                    Object it1 = value.get(i);
                    // 假設只支援二維陣列巢狀
                    tmpMap.put(transformDataToJSONAsOrderWay((Map<String, Object>) it1), i);
                }
                List<Object> orderedList = new ArrayList<>(tmpMap.size());
                for (Integer listNo : tmpMap.values()) {
                    orderedList.add(value.get(listNo));
                }
                transformedData.put(entry.getKey(), orderedList);
                continue;
            }
            transformedData.put(entry.getKey(), entry.getValue());
        }
        return JSONObject.toJSONString(transformedData);
    }
}

  以上就是完整的基於fastjson實現的json字典序維持的實現了,其實就是 transformDataToJSONAsOrderWay() 方法,其原理也簡單,因fastjson的有序性,依賴於輸入的資料結構,那麼只要維護好輸入結構的字典序就好了。TreeMap 是以字典序排序key的一種資料結構,符合這需求,另外,將list這種資料結構,轉化為kv這種資料結構,將整個item作為key排序後,再將其放入對應位置,從而保證了整體的順序性。但這種list的順序性,不一定是大家所理解的字典序,但一定可以保證得到相同的順序。

  另外,fastjson中還考慮了對於null值的處理,比如json中有null值的資料與沒有null值的資料,你說是相等呢還是不相等呢?

 

4. hashmap資料結構的順序迭代原理

  map是一種kv型的資料結構儲存,一般可以認為其是無序的。但我們可以額外的維護一些屬性,以保證它能夠以某種順序輸出資料,順序性主要體現在進行迭代時,如使用 keyset(), values(), entrySet() 等方法。針對額外維護順序性的資料結構而言,其迭代自然是基於其額外欄位。但針對無序的hashmap這種資料結構而言,我們知道其底層資料是根據hash值亂序儲存的。簡單來說就是根據一個hash值,然後求餘定位到一個陣列下標中。即對hashmap所分配的陣列物件的下標,有可能有值,有可能沒有值,那麼在做迭代的時候如何做呢?多次做迭代的順序一致嗎?一個最簡單的思路自然是依次遍歷資料的每個元素,直到資料的最大值。這樣,肯定是可以保證多次遍歷的順序性的。那麼,hashmap是否是這樣實現的呢?

    // java.util.HashMap#forEach
    @Override
    public void forEach(BiConsumer<? super K, ? super V> action) {
        Node<K,V>[] tab;
        if (action == null)
            throw new NullPointerException();
        if (size > 0 && (tab = table) != null) {
            int mc = modCount;
            // 1. 迭代所有陣列元素
            // 2. 迭代hash衝突時的連結串列
            for (int i = 0; i < tab.length; ++i) {
                for (Node<K,V> e = tab[i]; e != null; e = e.next)
                    action.accept(e.key, e.value);
            }
            if (modCount != mc)
                throw new ConcurrentModificationException();
        }
    }
        // java.util.HashMap.EntrySet#forEach
        public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
            Node<K,V>[] tab;
            if (action == null)
                throw new NullPointerException();
            if (size > 0 && (tab = table) != null) {
                int mc = modCount;
                for (int i = 0; i < tab.length; ++i) {
                    for (Node<K,V> e = tab[i]; e != null; e = e.next)
                        action.accept(e);
                }
                if (modCount != mc)
                    throw new ConcurrentModificationException();
            }
        }
        // keySet(), 換成了取key的處理
        // java.util.HashMap.KeySet#forEach
        public final void forEach(Consumer<? super K> action) {
            Node<K,V>[] tab;
            if (action == null)
                throw new NullPointerException();
            if (size > 0 && (tab = table) != null) {
                int mc = modCount;
                for (int i = 0; i < tab.length; ++i) {
                    for (Node<K,V> e = tab[i]; e != null; e = e.next)
                        action.accept(e.key);
                }
                if (modCount != mc)
                    throw new ConcurrentModificationException();
            }
        }
        // values() 換成了取value的處理
        // java.util.HashMap.Values#forEach
        public final void forEach(Consumer<? super V> action) {
            Node<K,V>[] tab;
            if (action == null)
                throw new NullPointerException();
            if (size > 0 && (tab = table) != null) {
                int mc = modCount;
                for (int i = 0; i < tab.length; ++i) {
                    for (Node<K,V> e = tab[i]; e != null; e = e.next)
                        action.accept(e.value);
                }
                if (modCount != mc)
                    throw new ConcurrentModificationException();
            }
        }

  TreeMap基於key做排序處理,最符合有序性要求,其迭代實現如下:

    // java.util.TreeMap#forEach
    @Override
    public void forEach(BiConsumer<? super K, ? super V> action) {
        Objects.requireNonNull(action);
        int expectedModCount = modCount;
        // 從最小的key開始取,進行二叉樹的中序遍歷
        // 因該二叉樹在插入時維護了有序性,進行遍歷時也就有了順序了
        for (Entry<K, V> e = getFirstEntry(); e != null; e = successor(e)) {
            action.accept(e.key, e.value);

            if (expectedModCount != modCount) {
                throw new ConcurrentModificationException();
            }
        }
    }
    
    /**
     * Returns the successor of the specified Entry, or null if no such.
     */
    static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
        if (t == null)
            return null;
        else if (t.right != null) {
            Entry<K,V> p = t.right;
            while (p.left != null)
                p = p.left;
            return p;
        } else {
            Entry<K,V> p = t.parent;
            Entry<K,V> ch = t;
            while (p != null && ch == p.right) {
                ch = p;
                p = p.parent;
            }
            return p;
        }
    }

  LinkedHashMap是按照插入的順序排列的一種map, 它與ArrayList這種有序性有相似性,但相對難以理解一些。因為list這種資料結構,你說先插入哪個元素,後插入哪個元素,是顯而易見的。然而像map這種資料結構,你很想像它是先插入某元素,再插入另一個元素的,這是一種先入為主的概念導致的。但它並不影響我們理解map有序性的實現,LinkedHashMap的迭代實現如下:

    // java.util.LinkedHashMap#forEach
    public void forEach(BiConsumer<? super K, ? super V> action) {
        if (action == null)
            throw new NullPointerException();
        int mc = modCount;
        // 僅通過維護插入時的連結串列,即可實現有序迭代
        for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
            action.accept(e.key, e.value);
        if (modCount != mc)
            throw new ConcurrentModificationException();
    }

  ok, 到此我們分析了多個型別的map的有序性的實現。從內部解釋了為什麼我們使用TreeMap資料結構時,就可以使json保持字典序了。因為fastjson在寫json資料時,針對map的寫入,就是通過entrySet()迭代元素進行寫入的了。

 

相關文章