大家都懂的 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 解析器,它是一個函數。一個函數就能搞定嗎?——如果只考慮 JSON 簡單情況(此種情況固然是不能放在生產環境的)是可以的,而且代碼行數少,正好適合我們初學理解。下面是該函數的完整代碼。
/**

怎麼理解這個函數呢?首先方法輸入的是字符串,我們把字符串“打散”,也就是 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 解析。今天先說到這兒,裏面的內容有不少地方是需要好好消化的。如果沒有幫到讀者理解,或者有進一步的問題,都可以跟在下溝通。歡迎交流!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章