你們都懂的 JSON 解析器原理(一)簡介 & 低配版入門

沒學過編譯原理,作一個 JSON 解析器難嗎?——難!是否是就不能「迎難而上」呢?——不是!越是難的越是一個挑戰!——筆者這裏嘗試經過通俗易懂的行文爲你們介紹一下 JSON 解析器,——那一串串長長的 JSON 文本究竟是如何被解析成爲 Java 裏面「能夠理解的」對象的。前面的鋪墊可能比較長,但請儘可能不要跳過,由於那都是基礎,尤爲對於咱們非科班來講,應要惡補。固然,爲照顧你們的理解程度(包括我本身,我也會之後回看本身的代碼,以此反覆理解、反覆消化),我會把代碼寫多點註釋,把代碼可讀性提升那麼一點點,由於網上不少寫解析器的大神都是從 C 語言高手過來的,明顯帶有過程式的風格。所以我會重構這些代碼,使得代碼更 OO 一些,這樣看起來也會緊湊一些,可讀性高一些。程序員

目標
輸入 JSON 字符串,對象或數組相互嵌套着,如:
{
"firstName": "John",
"lastName": "Smith",
"age": 25,
"address": {
"streetAddress": "21 2nd Street",
"city": "New York",
"state": "NY",
"postalCode": 10021
},
"phoneNumbers": [
{
"type": "home",
"number": "212 555-1234"
},
{
"type": "fax",
"number": "646 555-4567"
}
]
}
能夠 {} 包含 [],也能夠 [] 包含 {},總之相互嵌套,最後到 Java 返回 Map 或 List 就能夠了——固然 Java 裏的 Map or List 也是能夠相互嵌套着的。json

要求知識數組

讀者應當對 JSON 結構是怎麼一回事要了然於胸。
讀者應當瞭解數據結構中的棧。若是沒有了解,不要緊,能夠先讀讀筆者博文《用 JSON 表現樹的結構兼談隊列、堆棧的練習》。
好吧,正式開始!數據結構

低配版,一個函數搞定
這是來自 「安西都護府首席程序員」的方法。app

能夠說這是一個超簡單 JSON 解析器,它是一個函數。一個函數就能搞定嗎?——若是隻考慮 JSON 簡單狀況(此種狀況當然是不能放在生產環境的)是能夠的,並且代碼行數少,正好適合咱們初學理解。下面是該函數的完整代碼。
/**ide

  • @param jsonstring
  • @returnbr/>*/
    @SuppressWarnings("unchecked")
    public static Object json2Map(String jsonstring) {
    char[] cs = jsonstring.toCharArray();
    Stack<Map> maps = new Stack<>(); //用來表示多層的json對象
    Stack<List> lists = new Stack<>(); //用來表示多層的list對象
    Stack<Boolean> islist = new Stack<>();//判斷是否是list
    Stack<String> keys = new Stack<>(); //用來表示多層的key函數

    String keytmp = null;
    Object valuetmp = null;
    StringBuilder builder = new StringBuilder();post

    for (int i = 0; i < cs.length; i++) {ui

    switch (cs[i]) {
        case '{': //若是是{map進棧
            maps.push(new HashMap());
            islist.push(false);
            break;
        case ':'://若是是:表示這是一個屬性建,key進棧
            keys.push(builder.toString());
            builder = new StringBuilder();
            break;
        case '[':
            lists.push(new ArrayList());
            islist.push(true);
            break;
        case ',':
            if (builder.length() > 0)
                valuetmp = builder.toString();
            builder = new StringBuilder();
    
            boolean listis = islist.peek();
            if (!listis) {
                keytmp = keys.pop();
                maps.peek().put(keytmp, valuetmp);
            } else
                lists.peek().add(valuetmp);
    
            break;
        case ']':
            islist.pop();
    
            if (builder.length() > 0)
                valuetmp = builder.toString();
            lists.peek().add(valuetmp);
            valuetmp = lists.pop();
            builder = new StringBuilder();
            break;
        case '}':
            islist.pop();
            //這裏作的和,作的差很少,只是須要把valuetmp=maps.pop();把map彈出棧
            keytmp = keys.pop();
    
            if (builder.length() > 0)
                valuetmp = builder.toString();
    
            builder = new StringBuilder();
            maps.peek().put(keytmp, valuetmp);
            valuetmp = maps.pop();
            break;
        default:
            builder.append(cs[i]);
            break;
    }

    }
    return valuetmp;
    }
    該函數輸入一個 String 類型的參數,返回一個 Object 類型結果。Object 類型只有兩種真實類型,要麼是 Map,要麼是 List,分別對應最外層的 JSON 類型。code

怎麼理解這個函數呢?首先方法輸入的是字符串,咱們把字符串「打散」,也就是 char[] cs=jsonstring.toCharArray(); 這句把字符串轉換爲字符數組。變成數組的目的是要遍歷也就是把數組中的每個字符都讀出來。讀了一個字符,並進行解析。解析完畢了,咱們叫「消耗」。把這個字符消耗了,接着就讀取下一個字符重複上述過程。如此 JSON 裏面每個字符都會被讀取、解析、消耗。

將字符串變爲字符數組,實際上不少 JSON 解析庫都會那麼作,是爲第一步之工序。獲得 char[] 而後遍歷它,其中的遍歷過程就是具體的一個解析 JSON 的過程。

至於遍歷 for 裏面具體怎麼個解析法?此當然是要重點探討的話題。
解析過程
棧結構的運用
很多非科班的童鞋一聽到棧(Stack)就頭大了。其實棧沒想象中複雜,關鍵在於怎麼把它運用起來,體會了它的真正用途,而不是雲裏霧裏的概念。你能夠把棧想象成食堂中的一堆餐盤,一般咱們都是在餐盤頂部添加新餐盤(常識),而後取出餐盤就是從餐盤堆頂部拿出。這個即是棧的「後進先出」特性了。理解這個例子的意思當然淺顯,但怎麼和實際計算機問題結合起來呢——那又是一個問題。若是你們仍是不理解,能夠讀一下我前面的博文《用 JSON 表現樹的結構兼談隊列、堆棧的練習》,特別是最後一個 format json 的例子,雖然沒有直接運用到 Stack 結構但其中已隱隱約約有種「一進一退」的思想,着實與 Stack 有「殊途同歸」之類似。

函數中一口氣聲明瞭 4個 Stack:

Stack<Map<String, Object>> maps = new Stack<>(); // 用來保存全部父級對象
Stack<List<Object>> lists = new Stack<>(); // 用來保存全部父級數組
Stack<Boolean> isList = new Stack<>();// 判斷是否是list
Stack<String> keys = new Stack<>(); // 用來表示多層的key

咱們知道 JSON 乃樹狀結構。樹樁結構的特色是父親節點擁有子節點,子節點的上一級是父節點,造成了這種關係。變量 maps 用於記住遍歷字符的時候,字符所在在父級對象有哪些。父級節點 maps 是一個集合的概念,由於可能不止一個父級節點,並且可能有 n 個,那個 n 就表明樹的層數。且 maps 裏面的順序不能打亂(不過能夠放心,在 Stack 裏面並不容許「打亂」順序)。

同理,遇到數組的方式也能夠這樣去理解,保存在 lists 變量中。

固然,必須先有父級節點,纔會有子節點,不然子節點就沒有容身的「場所」。故而第一個欲消耗的字符永遠要麼是 {,永遠要麼是 [,纔會 new 第一個 map 對象或者 list 對象。第一個 { 或 [ 能夠稱爲「根節點」或「頂級節點」。

回到函數中,分別是以下進行字符的消耗的:

switch (cs[i]) {
case '{': // 若是是 { map 進棧
maps.push(new HashMap<String, Object>());
isList.push(false);
continue;
……
……
case '[':
isList.push(true);
lists.push(new ArrayList<Object>());
continue;

咱們忽略 switch 中不相關的部分,用省略號表示。可見,一遇到 { 字符,就表示要新建 map 對象,並且要將 map 進棧到 maps 中;一遇到 [ 字符,就表示要新建 list 對象,並且要將 list 進棧到 lists 中。進棧的意思就是在棧頂部添加新的元素。

光有進棧不夠,應該還有「退棧」的那麼一個操做。不過這裏權且埋下伏筆,回過頭來咱們再看退棧。

結對匹配
上述過程就是匹配 JSON 字符串中的兩種括號:尖括號和方括號,如 [ { }, [ ], [ ] ] 或 { [ ], [ ] } 等爲正確格式,[ { ] } 或 { [ } } 爲不合法格式。咱們把 JSON 字符串抽象成這個格式去理解,有助於咱們理解怎麼匹配成對出現的結構。

例如考慮下面的括號序列。

[ { [ ] [ ] } ]
1 2 3 4 5 6 7 8

當消耗了第 1 個括號 [ 以後,期待與它匹配的第 8 個括號 ] 出現,然而等來的倒是第 2 括號 {,此時第 1 個括號只能靠邊站,不過不要緊,由於咱們消耗過程當中已經把它保存起來,進行過「入棧」了;好,接着第 2 個括號要匹配的是 },可是很遺憾,第 3 個括號並非期待的 },而是 [。不過一樣不要緊,由於第 2 個括號已經保存起來,先記着;如今輪到第 3 個括號,就要看看第 4 個括號怎麼樣?第 4 個括號正好是 ],完成匹配!期待獲得了知足!可是不要忘記剛纔第 3 個括號已經入過棧,因此如今知足以後,當前就不是原來的位置——須要執行什麼操做?就是要「退棧」的操做。

執行完退棧以後,當前位置是第 5 個括號,而當前所期待的括號理應是第 2 個括號的期待,這個期待最爲迫切。不過很遺憾,第 2 個括號還必須「忍一忍」,由於第 5 個括號是 [,說明又有新的期待進來,迫切性更高,第 2 個括號必須「讓位於」第 5 個括號。——這裏咱們假設是故意弄錯,第 6 個括號進入的是一個右尖括號 },明顯這樣不能構成結對,是非法字符,因而應停止遍歷,馬上報錯。回到正確的例子上,咱們看到第 6 個括號是合法的括號,完成匹配,接下來期待第 2 個括號的匹配,或者是 [ or { 新開一級的匹配——這都是能夠、合法的。

因而可知,這過程與棧的結構相吻合。「一進一退」是必須完成的結對,不然是不合法的過程。

只有掌握了這個匹配過程,咱們才能進入下一步的 JSON 解析。今天先說到這兒,裏面的內容有很多地方是須要好好消化的。若是沒有幫到讀者理解,或者有進一步的問題,均可以跟在下溝通。歡迎交流!

相關文章
相關標籤/搜索