因爲我對寫解析器只有 閱讀了幾篇文章 的知識量,所以水平並非很高,此文權當一次我的總結,沒法保證所涉及的知識點、思路徹底無誤,若有錯誤,還請各位大佬指正。java
這篇文章圍繞的僅僅是一個 正整數表達式,並且它很簡單,不會出現括號嵌套等狀況,咱們的目標只是把express
10 * 5 + 1
解析爲一個 Token
序列,以下:json
[ { type: NUMBER, value: `10` }, { type: OPERATOR, value: `*` }, { type: NUMBER, value: `5` }, { type: OPERATOR, value: `+` }, { type: NUMBER, value: `1` } ]
我習慣從簡單的開始,那麼咱們先從一個最簡單的、只有個位數、沒有空格的式子開始:app
1+1
其實詞法分析器要作的事本質上很簡單:對輸入的字符串進行遍歷,分割成有意義的 Token
。
所以,最簡單的思路就是一個 for
循環:ide
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; } }
同時該枚舉類承擔辨識字符類型的工做:this
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
如今一個數字可能不止一位了,那麼咱們該怎麼辦呢?code
使用狀態圖:orm
┌-[ 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
。
同時,咱們還發現引入狀態圖後,有個有意思的事:
從 初始狀態 INIT
開始,咱們只容許後邊是 NUMBER
類型;NUMBER
後邊容許 NUMBER
、OPERATOR
類型;OPERATOR
後邊容許 NUMBER
類型。
除此以外的狀態都是不合法的,這也就是有時候解析類的包(好比 fast-json
)會看到的 Invalid Character
錯誤的狀況。
因此咱們須要改改代碼,同時爲 Scanner
添加一個更新狀態的方法:
public class Scanner { // 省略 private Token token; public void setToken(Token token) { this.token = token; } public void process() { // 初始化 this.setToken(new Token(Type.INIT)); for (char ch : expression.toCharArray()) { Type type = Type.of(ch); if (Type.UNKNOWN.equals(type)) { throw new RuntimeException(String.format("`%c` 並不屬於指望的字符類型", ch)); } // 根據當前 Token 的類型,選擇不一樣的判斷分支 switch (token.getType()) { case INIT: switch (type) { // 當前是初始狀態,遇到了數字, 切換狀態。 case NUMBER: this.setToken(new Token(Type.NUMBER)); this.token.appendValue(ch); break; default: throw new RuntimeException(String .format("Invalid Character: `%c`", ch)); } break; case NUMBER: switch (type) { // 當前是數字狀態,遇到了數字,追加字符。 case NUMBER: this.token.appendValue(ch); break; // 當前是數字狀態,遇到了操做符,切換狀態。 case OPERATOR: this.setToken(new Token(Type.OPERATOR)); this.token.appendValue(ch); break; default: throw new RuntimeException(String .format("Invalid Character: `%c`", ch)); } break; case OPERATOR: switch (type) { // 當前是操做符狀態,遇到了數字,切換狀態。 case NUMBER: this.setToken(new Token(Type.NUMBER)); this.token.appendValue(ch); break; default: throw new RuntimeException(String .format("Invalid Character: `%c`", ch)); } break; } System.out.println(token); } } }
/** 輸出 * {type: NUMBER, value: `1`} * {type: NUMBER, value: `10`} * {type: OPERATOR, value: `+`} * {type: NUMBER, value: `1`} */
咱們剛纔用了一個巨大無比的 switch
結構來描述狀態圖,如今咱們由內而外試着簡化這個巨無霸。
switch (type) { // 當前是 ** 狀態,遇到了 ** , 執行 ** 操做。 case NUMBER: // ... break; case ... default: throw new RuntimeException(String.format("Invalid Character: `%c`", ch)); }
其實稍微概括總結一下就能發現, 執行 ** 操做
這部分,總的來講只有兩種:
NewToken 對應着 { token = new Token(Type.NUMBER); token.appendValue(ch); } AppendValue 對應着 { token.appendValue(ch); }
如今咱們再引入一個工具來幫助咱們簡化:表驅動。
其實從上面的對應關係不難發現,咱們能夠用 HashMap
來簡單模擬一個表,幫助咱們減小工做。
在此以前,咱們須要把上述關係中的 {操做}
部分用一個接口來解耦:
public interface Behavior { void apply(Token token, Type type, char ch); }
而後咱們來定義一個枚舉類 Behaviors
來表示操做類型:
public enum Behaviors { NewToken, AppendValue; private static final Scanner scanner; private static final HashMap<Behaviors, Behavior> behaviorMap; static { scanner = Scanner.getInstance(); behaviorMap = new HashMap<>(); behaviorMap.put(Behaviors.NewToken, (token, type, ch) -> { token = new Token(type); token.appendValue(ch); scanner.setToken(token); }); behaviorMap.put(Behaviors.AppendValue, (token, type, ch) -> { token.appendValue(ch); }); } public void apply(Token token, Type type, char ch) { behaviorMap.get(this) .apply(token, type, ch); } }
那麼如今 執行 操做 這部分,如今能夠用 HashMap
來表述了:
// 根據當前 Token 的類型,選擇不一樣的判斷分支 switch (token.getType()) { case INIT: HashMap<Type, Behaviros> behaviorsMap = new HashMap<>(); // 當前是初始狀態,遇到了數字, 切換狀態。 behaviorsMap.put(Type.NUMBER, Behaviors.NewToken); break; case NUMBER: HashMap<Type, Behaviros> behaviorsMap = new HashMap<>(); // 當前是數字狀態,遇到了數字,追加字符。 behaviorsMap.put(Type.NUMBER, Behaviors.AppendValue); // 當前是數字狀態,遇到了操做符,切換狀態。 behaviorsMap.put(Type.Operator, Behaviors.NewToken); break; case OPERATOR: HashMap<Type, Behaviros> behaviorsMap = new HashMap<>(); // 當前是操做符狀態,遇到了數字,切換狀態。 behaviorsMap.put(Type.NUMBER, Behaviors.NewToken); break; }
既然是 Java
,那麼讓咱們來讓這部分看起來 OO
一些:
public class BehaviorMap { private final HashMap<Type, Behaviors> map; public BehaviorMap() { this.map = new HashMap(); } public BehaviorMap at(Type type, Behaviors behaviors) { this.map.put(type, behaviors); return this; } public BehaviorsTable done() { return BehaviorsTable.getInstance(); } }
如今再來看看:
// 根據當前 Token 的類型,選擇不一樣的判斷分支 switch (token.getType()) { case INIT: BehaviorMap map = new BehaviorMap(); map // 當前是初始狀態,遇到了數字, 切換狀態。 .at(Type.NUMBER, Behaviors.NewToken); break; case NUMBER: BehaviorMap map = new BehaviorMap(); map // 當前是數字狀態,遇到了數字,追加字符。 .at(Type.NUMBER, Behaviors.AppendValue); // 當前是數字狀態,遇到了操做符,切換狀態。 .at(Type.Operator, Behaviors.NewToken); break; case OPERATOR: BehaviorMap map = new BehaviorMap(); map // 當前是操做符狀態,遇到了數字,切換狀態。 .at(Type.NUMBER, Behaviors.NewToken); break; }
如今咱們能夠看到表驅動對於消除判斷分支的威力了,那麼咱們能夠用一樣的方法將外部 switch
也消除掉:
public class BehaviorsTable { private static volatile BehaviorsTable instance; public static BehaviorsTable getInstance() { if (BehaviorsTable.instance == null) { synchronized ( BehaviorsTable.class ) { if (BehaviorsTable.instance == null) { BehaviorsTable.instance = new BehaviorsTable(); } } } return BehaviorsTable.instance; } private final HashMap<Type, BehaviorMap> map; public BehaviorsTable () { this.map = new HashMap<>(); } public BehaviorMap register(Type type) { BehaviorMap behaviorMap = new BehaviorMap(); this.map.put(type, behaviorMap); return behaviorMap; } }
如今整個巨大的 switch
結構咱們就能夠簡化爲:
BehaviorsTable .getInstance() .register(Type.INIT) .at(Type.NUMBER, Behaviors.NewToken) .done() .register(Type.NUMBER) .at(Type.NUMBER, Behaviors.AppendValue) .at(Type.OPERATOR, Behaviors.NewToken) .done() .register(Type.OPERATOR) .at(Type.NUMBER, Behaviors.NewToken) .done();
如今 process
方法咱們就能夠簡化爲:
public void process() { this.setToken(new Token(Type.INIT)); for (char ch : expression.toCharArray()) { Type type = Type.of(ch); if (Type.UNKNOWN.equals(type)) { throw new RuntimeException(String.format("`%c` 並不屬於指望的字符類型", ch)); } BehaviorsTable .getInstance() // 根據當前 Token 類型獲取對應的處理對策 .get(this.token.getType()) // 獲取當前字符所屬的處理行爲 .is(type) .apply(type, ch); System.out.println(token); } }
咱們在源碼裏作了一些改動,請參考文章底部所有代碼。
public static void main(String ... args) { Scanner scanner = Scanner.getInstance(); scanner.from("10+1").process(); /** * {type: NUMBER, value: `1`} * {type: NUMBER, value: `10`} * {type: NUMBER, value: `+`} * {type: NUMBER, value: `1`} */ scanner.from("10 +1").process(); /** * {type: NUMBER, value: `1`} * {type: NUMBER, value: `10`} * Exception in thread "main" java.lang.RuntimeException: ` ` 並不屬於指望的字符類型 */ scanner.from("10++1").process(); /** * {type: NUMBER, value: `1`} * {type: NUMBER, value: `10`} * {type: OPERATOR, value: `+`} * Exception in thread "main" java.lang.RuntimeException: Invalid Character: `+` for Token `OPERATOR` */ }
如今看起來一切正常,可是別忘了 Scanner
的工做是將輸入的字符串分割爲 Token
序列,所以咱們須要讓 process
方法返回處理後的 LinkedList<Token>
序列。
爲此咱們須要將每次新生成的 Token
保存下來:
public class Scanner { private LinkedList<Token> tokens; private Token token; public void addToken(Token token) { this.token = token; this.tokens.add(token); } public LinkedList<Token> process() { this.setToken(new Token(Type.INIT)); for (char ch : expression.toCharArray()) { Type type = Type.of(ch); if (Type.UNKNOWN.equals(type)) { throw new RuntimeException(String.format("`%c` 並不屬於指望的字符類型", ch)); } BehaviorsTable .getInstance() .get(this.token.getType()) .is(type) .apply(type, ch); } return this.tokens; } public static void main(String ... args) { Scanner scanner = Scanner.getInstance().from("10*5+1"); LinkedList<Token> tokens = scanner.process(); for (Token token : tokens) { System.out.println(token); } } }
記得將 Behaviors
初始化部分中 NewToken
裏的行爲修改一下:
behaviorMap.put(Behaviors.NewToken, (token, type, ch) -> { token = new Token(type); token.appendValue(ch); //scanner.setToken(token); scanner.addToken(token); });
如今再看看結果:
{type: NUMBER, value: `10`} {type: OPERATOR, value: `*`} {type: NUMBER, value: `5`} {type: OPERATOR, value: `+`} {type: NUMBER, value: `1`}
看起來一切都如咱們所願!如今離最初的目標只剩下空格的處理了,得益於咱們抽象了行爲 Behaviors
,咱們只須要在 Type
中註冊空格,而後爲 BehaviorsTable
註冊各類類型下對空格的處理就好了:
public enum Type { SPACE; public Tpye of(char ch) { if (' ' == ch) return SPACE; } } public enum Behaviors { Continue; static { behaviorMap .put(Behaviors.Continue, (token, type, ch) -> { // 留空就好了 }) } } public class Scanner { static { BehaviorsTable .getInstance() .register(Type.INIT) .at(Type.NUMBER, Behaviors.NewToken) .at(Type.SPACE, Behaviors.Continue) .done() .register(Type.NUMBER) .at(Type.NUMBER, Behaviors.AppendValue) .at(Type.OPERATOR, Behaviors.NewToken) .at(Type.SPACE, Behaviors.Continue) .done() .register(Type.OPERATOR) .at(Type.NUMBER, Behaviors.NewToken) .at(Type.SPACE, Behaviors.Continue) .done(); } }
咱們如今完成的掃描器實際上沒法識別出 1 1 + 0
是個錯誤的表達式,它會解析出以下序列:
{type: NUMBER, value: 11} {type: OPERATOR, value: +} {type: NUMBER, value: 0}
我我的但願這部分工做往上層放,由消化 Token
序列的調用者經過模式匹配的方式去驗證,不過這樣的話,Type.SPACE
的處理就不能隨意 Continue
了,有興趣的話看官能夠自行嘗試一下 : P
一個小嚐試,就不傳 Github 了,直接放這兒吧 (其實就是懶...
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; } static { // 註冊行爲表 BehaviorsTable .getInstance() // 註冊 INIT 狀態的行爲表 .register(Type.INIT) .at(Type.NUMBER, Behaviors.NewToken) .at(Type.SPACE, Behaviors.Continue) .done() .register(Type.NUMBER) .at(Type.NUMBER, Behaviors.AppendValue) .at(Type.OPERATOR, Behaviors.NewToken) .at(Type.SPACE, Behaviors.Continue) .done() .register(Type.OPERATOR) .at(Type.NUMBER, Behaviors.NewToken) .at(Type.SPACE, Behaviors.Continue) .done(); } private String expression; private LinkedList<Token> tokens; private Token token; public Scanner from(String expression) { this.expression = expression; this.tokens = new LinkedList<>(); return this; } public void setToken(Token token) { this.token = token; } public Token getToken ( ) { return token; } public LinkedList<Token> process() { this.setToken(new Token(Type.INIT)); for (char ch : expression.toCharArray()) { Type type = Type.of(ch); if (Type.UNKNOWN.equals(type)) { throw new RuntimeException(String.format("`%c` 並不屬於指望的字符類型", ch)); } BehaviorsTable .getInstance() // 獲取當前 Token 類型所適用的行爲表 .get(this.token.getType()) // 獲取當前字符所適用的行爲 .is(type) .apply(type, ch); } return this.tokens; } public void addToken(Token token) { // 更新一下當前 Token this.token = token; this.tokens.add(token); } public static void main(String ... args) { Scanner scanner = Scanner.getInstance().from("10 * 5+1"); LinkedList<Token> tokens = scanner.process(); for (Token token : tokens) { System.out.println(token); } } }
// Token 類型枚舉 public enum Type { INIT, // 初始化時使用 SPACE, // 空格 NUMBER, // 數字 OPERATOR, // 操做符 UNKNOWN; // 未知類型 public static Type of(char ch) { if (' ' == ch) { return SPACE; } if ('0' <= ch && ch <= '9') { return NUMBER; } if ("+-*/".indexOf(ch) != -1) { return OPERATOR; } return UNKNOWN; } }
public class Token { // 一個 Token 的類型一旦肯定,就不可能再改變。 private final Type type; // 用以存儲 Token 的值。 private final StringBuffer value; public Token(Type type) { this.type = type; this.value = new StringBuffer(); } // 向 value 中追加字符 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()); } }
public interface Behavior { /** * 將行爲抽象出來 * @param token 當前的 token * @param type 讀入字符的類型 * @param ch 讀入的字符 */ void apply(Token token, Type type, char ch); }
// 預設行爲 public enum Behaviors { NewToken, // 新建一個指定類型的 Token, 將當前字符保存到新 Token AppendValue, // 將當前字符追加到當前 Token 的值中 Continue, // 跳過當前字符 InvalidCharacter; // 當前 Token 類型所不指望的字符類型,會拋出一個異常 // 持有一個引用就不用總是調用 getInstance()。 private static final Scanner scanner; // 爲預設行爲指定行爲內容 private static final HashMap<Behaviors, Behavior> behaviorMap; static { scanner = Scanner.getInstance(); behaviorMap = new HashMap<>(); // 指定 NewToken,行爲邏輯參見枚舉值說明 behaviorMap.put(Behaviors.NewToken, (token, type, ch) -> { token = new Token(type); token.appendValue(ch); scanner.addToken(token); }); // 指定 AppendValue,行爲邏輯參見枚舉值說明 behaviorMap.put(Behaviors.AppendValue, (token, type, ch) -> { token.appendValue(ch); }); // 指定 Continue,行爲邏輯參見枚舉值說明 behaviorMap.put(Behaviors.Continue, (token, type, ch) -> {}); // 指定 InvalidCharacter,行爲邏輯參見枚舉值說明 behaviorMap.put(Behaviors.InvalidCharacter, (token, type, ch) -> { throw new RuntimeException(String .format("Invalid Character: `%c` for Token `%s`", ch, token.getType().name())); }); } public void apply(Type type, char ch) { // 獲取預設行爲 behaviorMap.get(this) // 向行爲中傳遞當前 Token, 當前字符類型,當前字符 .apply(scanner.getToken(), type, ch); } }
// 保存某一字符類須要執行何種預設行爲的映射關係 public class BehaviorsMap { private final HashMap<Type, Behaviors> map; public BehaviorsMap() { this.map = new HashMap(); } /** * 註冊指定類型所需的預設行爲 * @param type 指定類型 * @param behaviors 指定所需的預設行爲 */ public BehaviorsMap at(Type type, Behaviors behaviors) { this.map.put(type, behaviors); return this; } // 註冊完後回退操做域到 BehaviorsTable public BehaviorsTable done() { return BehaviorsTable.getInstance(); } // 獲取指定類型的預設行爲 public Behaviors is (Type type) { Behaviors behaviors = this.map.get(type); if (behaviors == null) { // 若是沒有註冊,那麼使用 InvalidCharacter 預設行爲,由於出現了非預期的字符類型 behaviors = Behaviors.InvalidCharacter; } return behaviors; } }
// 行爲表 public class BehaviorsTable { private static volatile BehaviorsTable instance; public static BehaviorsTable getInstance() { if (BehaviorsTable.instance == null) { synchronized ( BehaviorsTable.class ) { if (BehaviorsTable.instance == null) { BehaviorsTable.instance = new BehaviorsTable(); } } } return BehaviorsTable.instance; } private final HashMap<Type, BehaviorMap> map; public BehaviorsTable () { this.map = new HashMap<>(); } // 註冊指定當前類型,返回一個空的 BehaviorsMap 來註冊預設行爲 public BehaviorsMap register(Type type) { BehaviorsMap behaviorsMap = new BehaviorsMap(); this.map.put(type, behaviorsMap); return behaviorsMap; } // 獲取指定當前類型的 BehaviorsMap public BehaviorsMap get(Type type) { return this.map.get(type); } }