写在前面
由于我对写解析器只有 阅读了几篇文章 的知识量,因此水平并不是很高,此文权当一次个人总结,无法保证所涉及的知识点、思路完全无误,如有错误,还请各位大佬指正。
从一个正整数表达式开始
这篇文章围绕的仅仅是一个 正整数表达式,而且它很简单,不会出现括号嵌套等情况,我们的目标只是把
10 * 5 + 1
解析为一个 Token 序列,如下:
[
{ type: NUMBER, value: `10` },
{ type: OPERATOR, value: `*` },
{ type: NUMBER, value: `5` },
{ type: OPERATOR, value: `+` },
{ type: NUMBER, value: `1` }
]
我习惯从简单的开始,那么我们先从一个最简单的、只有个位数、没有空格的式子开始:
1+1
最简单的思路
其实词法分析器要做的事本质上很简单:对输入的字符串进行遍历,分割成有意义的 Token。
因此,最简单的思路就是一个 for 循环:
String expression = "1+1";
for (char ch : expression.toCharArray()) {
// 在这里进行处理
}
所以我们定义一个 Scanner,为了后续方便,顺手实现个简单的单例吧:
public class Scanner {
private static volatile Scanner instance;
public static Scanner getInstance() {
if (Scanner.instance == null) {
synchronized ( Scanner.class ) {
if (Scanner.instance == null) {
Scanner.instance = new Scanner();
}
}
}
return Scanner.instance;
}
private String expression;
public Scanner from(String expression) {
this.expression = expression;
return this;
}
public void process() {
for (char ch : expression.toCharArray()) {
// 在这里进行处理
}
}
public static void main(String ... args) {
Scanner scanner = Scanner.getInstance().from("1+1");
scanner.process();
}
}
定义 Token 类型
在当前的 1+1 表达式中,涉及到的 Token 不多,只有数字、操作符,因此用一个枚举类即可表述:
public enum Type {
INIT,
NUMBER,
OPERATOR,
UNKNOWN;
public static Type of(char ch) {
if ('0' <= ch && ch <= '9') {
return NUMBER;
}
if ("+-*/".indexOf(ch) != -1) {
return OPERATOR;
}
return UNKNOWN;
}
}
同时该枚举类承担辨识字符类型的工作:
Type.of('1') // NUMBER
Type.of('+') // OPERATOR
Type.of('a') // UNKNOWN
定义 Token
public class Token {
// 一个 Token 的类型一旦确定,就不可能再改变。
private final Type type;
// 用以存储 Token 的值。
private final StringBuffer value;
public Token(Type type) {
this.type = type;
this.value = new StringBuffer();
}
public void appendValue(char ch) {
this.value.append(ch);
}
public String getValue() {
return this.value.toString();
}
public Type getType() {
return this.type;
}
@Override
public String toString() {
return String.format("{type: %s, value: `%s`}",
this.getType().name(),
this.getValue());
}
}
处理 1+1
public class Scanner {
// 省略...
public void process() {
for (char ch : expression.toCharArray()) {
Type type = Type.of(ch);
if (Type.UNKNOWN.equals(type)) {
throw new RuntimeException(String.format("`%c` 并不属于期望的字符类型", ch));
}
Token token = new Token(type);
token.appendValue(ch);
System.out.println(token);
}
}
public static void main(String ... args) {
Scanner scanner = new Scanner("1+1");
scanner.process();
}
}
/** 输出
* {type: NUMBER, value: `1`}
* {type: OPERATOR, value: `+`}
* {type: NUMBER, value: `1`}
*/
现在来加点难度: 10+1
现在一个数字可能不止一位了,那么我们该怎么办呢?
使用状态图:
┌-[ 0-9 ]-┐ ┌-[ +|-|*|/ ]-┐ ┌-[ 0-9 ]-┐
---( NUMBER )--- ( OPERATOR )---( NUMBER )---
具体的理论这里就不赘述了,有兴趣可以自行查阅相关资料,这里简单说一下怎么用:
现在我们来列个表,看一下对于 10+1,在状态上有什么变化:
字符
状态
Token
NULL
INIT
NULL
1
NUMBER
{id: 0, type: NUMBER, value: 1}
0
NUMBER
{id: 0, type: NUMBER, value: 10}
+
OPERATOR
{id: 1, type: OPERATOR, value: +}
1
NUMBER
{id: 2, type: NUMBER, value: 1}
可以看到,在读到字符 1 和 0 时,状态没有发生变化,也就是说它们是一个整体(或是一个整体的一部分)。
如果在 0 后面还有其他数字,那么直到引起状态改变的字符出现之前,这些字符就组成了整个 Token。
同时,我们还发现引入状态图后,有个有意思的事: