大家都懂的 JSON 解析器原理(一)简介 & 低配版入门

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: 没学过编译原理,做一个 JSON 解析器难吗?——难!是不是就不能“迎难而上”呢?——不是!越是难的越是一个挑战!——笔者这里尝试通过通俗易懂的行文为大家介绍一下 JSON 解析器,——那一串串长长的 JSON 文本到底是如何被解析成为 Java 里面“可以理解的”对象的。

没学过编译原理,做一个 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 简单情况(此种情况固然是不能放在生产环境的)是可以的,而且代码行数少,正好适合我们初学理解。下面是该函数的完整代码。

/**
 * 
 * @param jsonstring
 * @return
 */
@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();

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

        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 类型。

怎么理解这个函数呢?首先方法输入的是字符串,我们把字符串“打散”,也就是 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 解析。今天先说到这儿,里面的内容有不少地方是需要好好消化的。如果没有帮到读者理解,或者有进一步的问题,都可以跟在下沟通。欢迎交流!

目录
相关文章
|
17天前
|
存储 算法 Java
解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用
在Java中,Set接口以其独特的“无重复”特性脱颖而出。本文通过解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用。
35 3
|
30天前
|
SQL 存储 JSON
SQL,解析 json
SQL,解析 json
62 8
|
5天前
|
算法 Java 数据库连接
Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性
本文详细介绍了Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性。连接池通过复用数据库连接,显著提升了应用的性能和稳定性。文章还展示了使用HikariCP连接池的示例代码,帮助读者更好地理解和应用这一技术。
15 1
|
8天前
|
存储 弹性计算 NoSQL
"从入门到实践,全方位解析云服务器ECS的秘密——手把手教你轻松驾驭阿里云的强大计算力!"
【10月更文挑战第23天】云服务器ECS(Elastic Compute Service)是阿里云提供的基础云计算服务,允许用户在云端租用和管理虚拟服务器。ECS具有弹性伸缩、按需付费、简单易用等特点,适用于网站托管、数据库部署、大数据分析等多种场景。本文介绍ECS的基本概念、使用场景及快速上手指南。
36 3
|
10天前
|
数据采集 存储 编解码
一份简明的 Base64 原理解析
Base64 编码器的原理,其实很简单,花一点点时间学会它,你就又消除了一个知识盲点。
37 3
|
19天前
|
机器学习/深度学习 人工智能 自然语言处理
前端大模型入门(三):编码(Tokenizer)和嵌入(Embedding)解析 - llm的输入
本文介绍了大规模语言模型(LLM)中的两个核心概念:Tokenizer和Embedding。Tokenizer将文本转换为模型可处理的数字ID,而Embedding则将这些ID转化为能捕捉语义关系的稠密向量。文章通过具体示例和代码展示了两者的实现方法,帮助读者理解其基本原理和应用场景。
|
29天前
|
前端开发 Java 应用服务中间件
21张图解析Tomcat运行原理与架构全貌
【10月更文挑战第2天】本文通过21张图详细解析了Tomcat的运行原理与架构。Tomcat作为Java Web开发中最流行的Web服务器之一,其架构设计精妙。文章首先介绍了Tomcat的基本组件:Connector(连接器)负责网络通信,Container(容器)处理业务逻辑。连接器内部包括EndPoint、Processor和Adapter等组件,分别处理通信、协议解析和请求封装。容器采用多级结构(Engine、Host、Context、Wrapper),并通过Mapper组件进行请求路由。文章还探讨了Tomcat的生命周期管理、启动与停止机制,并通过源码分析展示了请求处理流程。
|
26天前
|
开发框架 缓存 前端开发
electron-builder 解析:你了解其背后的构建原理吗?
本文首发于微信公众号“前端徐徐”,详细解析了 electron-builder 的工作原理。electron-builder 是一个专为整合前端项目与 Electron 应用的打包工具,负责管理依赖、生成配置文件及多平台构建。文章介绍了前端项目的构建流程、配置信息收集、依赖处理、asar 打包、附加资源准备、Electron 打包、代码签名、资源压缩、卸载程序生成、安装程序生成及最终安装包输出等环节。通过剖析 electron-builder 的原理,帮助开发者更好地理解和掌握跨端桌面应用的构建流程。
58 2
|
27天前
|
域名解析 网络协议 安全
DNS查询工具简介
DNS查询工具简介
|
7天前
|
供应链 安全 分布式数据库
探索区块链技术:从原理到应用的全面解析
【10月更文挑战第22天】 本文旨在深入浅出地探讨区块链技术,一种近年来引起广泛关注的分布式账本技术。我们将从区块链的基本概念入手,逐步深入到其工作原理、关键技术特点以及在金融、供应链管理等多个领域的实际应用案例。通过这篇文章,读者不仅能够理解区块链技术的核心价值和潜力,还能获得关于如何评估和选择适合自己需求的区块链解决方案的实用建议。
25 0

推荐镜像

更多