你们都懂的 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 解析。今天先说到这儿,里面的内容有很多地方是须要好好消化的。若是没有帮到读者理解,或者有进一步的问题,均可以跟在下沟通。欢迎交流!

相关文章
相关标签/搜索