這節跟着書上第三章學習解析例如{val,val,{val,val},val}
這樣的數組初始化列表,其中每個val
都是一個無符號整數。它可以用來將Java中
static short[] data = {1,2,3};
轉化成等價的Unicode字符串形式:
static String data = "\u0001\u0002\u0003";
因爲Java中的char
就是unsigned short
,可以用Unicode字符(四個16進製表示的16位字符)來表達。
這樣轉換的目的是使數組初始化不受編譯出的class文件的限制。因爲class文件會將數組初始化顯式存儲爲賦值:
data[0] = 1;
data[1] = 2;
data[2] = 3;
而JVM規範規定,包括cinit
方法在內,一個方法的字節碼長度不能超過65535字節。這樣如果需要初始化的數組很長就會超過這個限制。
但是如果使用轉換後的字符串就沒有這個限制,因爲字符串常量存儲在class文件的常量池中,沒有長度限制。
1 從語法規則文件生成程序
這裏需要注意語法規則必須小寫字母開頭,一般直接用全小寫。而詞法符號規則必須大寫字母開頭,一般直接用全大寫。
這裏還有一個技巧,因爲生成的程序類名總是這個語法名+後綴拼接的方式,爲了符合Java類命名規範,這裏的語法名(和對應文件名) 也使用大寫字母開頭的駝峯式命名。
// 和文件名相同的語法名
grammar ArrayInit;
// ----------語法規則的定義(必須小寫字母開頭)----------
// 頂層規則
init:
'{' value (',' value)* '}' // 花括號內若干value用逗號隔開
;
// value的定義
value:
init // 可以是嵌套的花括號
| INT // 也可以是單獨一個INT詞法符號
;
// ----------詞法符號的定義(必須大寫字母開頭)----------
// 詞法符號INT的定義
INT : [0-9]+; // 一至多個數字
// 空白符號定義
WS : [ \t\r\n]+ -> skip; // 匹配到時將其丟棄
然後用筆記1中的插件方法自動生成程序,暫時不勾選生成visitor,書上是直接用命令行(antlr4 xxx.g4
)。
如果生成的程序中@Override
下面有紅線報錯,是IDEA的項目Language level設置得太低,在模塊上F4打開設置,然後將Language level設置爲6就可以了。
2 生成的程序文件概覽
2.1 ArrayInitParser.java
這個文件中存放語法分析器類:
public class ArrayInitParser extends Parser { ... }
用來解析ArrayInit這個語法文件中的語法規則,每條語法規則都對應其中的一個內部類(對應筆記2中學習的語法樹上的非葉子結點)和一個訪問方法,即:
public static class InitContext extends ParserRuleContext { ... }
public final InitContext init() throws RecognitionException { ... }
和
public static class ValueContext extends ParserRuleContext { ... }
public final ValueContext value() throws RecognitionException { ... }
另外還有一些輔助代碼。
2.2 ArrayInitLexer.java
這個文件中存放詞法分析器類:
public class ArrayInitLexer extends Lexer { ... }
它是通過分析.g4
文件中的詞法規則(INT
和WS
)以及語法規則中的字面值({
、}
和,
)生成的,用於將輸入的字符序列分解成詞法符號。
2.3 ArrayInit.tokens
這個文件用於存儲詞法符號到一個數字值的對應關係,每個詞法符號都會對應一個數字值,使用它可以在多個小型語法之間同步全部的詞法符號類型,以將一個大型語法切分成多個小型語法。
2.4 ArrayInitListener.java
該文件用於存儲監聽器接口:
public interface ArrayInitListener extends ParseTreeListener { ... }
在遍歷語法分析樹時,遍歷到指定的結點會觸發一系列事件回調,該接口即存放這些回調方法的定義。具體地,對於這個ArrayInit語法的語法樹而言,這包括:
InitContext
結點的進入和退出:
void enterInit(ArrayInitParser.InitContext ctx);
void exitInit(ArrayInitParser.InitContext ctx);
ValueContext
結點的進入和退出:
void enterValue(ArrayInitParser.ValueContext ctx);
void exitValue(ArrayInitParser.ValueContext ctx);
進入/退出哪個結點,就會調用上面相應的方法,並且把那個結點的上下文對象作爲參數傳進去。
2.5 ArrayInitBaseListener.java
該文件用於存儲監聽器接口的默認實現類:
public class ArrayInitBaseListener implements ArrayInitListener { ... }
也就是ANTLR提供的ArrayInitListener
接口的默認實現,可以看到裏面的回調方法都是空的,只需要按照需求覆蓋感興趣的回調方法,就能用監聽器的方式來訪問語法分析樹了。
3 構建從數組到字符串的翻譯程序
這裏通過繼承前面生成的監聽器類ArrayInitBaseListener
,在語法規則回調時翻譯對應的部分,然後直接打印出來:
實現監聽器:
import antlr.ArrayInitBaseListener;
import antlr.ArrayInitParser;
// 繼承監聽器類,覆蓋回調函數
public class ShortToUnicodeString extends ArrayInitBaseListener {
// 在init結點開始遍歷之前,把'{'翻譯的'"'輸出
@Override
public void enterInit(ArrayInitParser.InitContext ctx) {
System.out.print('"');
}
// 在init結點結束遍歷之後,把'}'翻譯的'"'輸出
@Override
public void exitInit(ArrayInitParser.InitContext ctx) {
System.out.print('"');
}
// 在value結點開始遍歷之前(之後也可以),把數字翻譯出的Unicode輸出
@Override
public void enterValue(ArrayInitParser.ValueContext ctx) {
// 假設不存在嵌套,value只走INT的分支
// 這裏ctx.INT().getText()從上下文對象中獲取這個結點的文本
int value = Integer.parseInt(ctx.INT().getText());
// 輸出Unicode(數字的4位16進製表示)
System.out.printf("\\u%04x", value);
}
}
在主類調用:
import antlr.ArrayInitLexer;
import antlr.ArrayInitParser;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.tree.ParseTreeWalker;
public class Main {
public static void main(String[] args) {
// 詞法分析、語法分析並得到語法樹(的根節點上下文對象)
ArrayInitLexer lexer = new ArrayInitLexer(CharStreams.fromString("{1,2,3,4}"));
ArrayInitParser parser = new ArrayInitParser(new CommonTokenStream(lexer));
ArrayInitParser.InitContext tree = parser.init();
System.out.println(tree.toStringTree(parser));
// 創建一個能觸發回調函數的語法分析樹遍歷器
ParseTreeWalker parseTreeWalker = new ParseTreeWalker();
// 將監聽器和語法樹傳入walk方法,該方法會遍歷語法樹觸發回調
parseTreeWalker.walk(new ShortToUnicodeString(), tree);
}
}
運行結果:
(init { (value 1) , (value 2) , (value 3) , (value 4) })
"\u0001\u0002\u0003\u0004"
這樣就完成了最初設想的,從一維數組到Unicode字符串的翻譯器。最後記錄一個坑點,如果在Java程序的註釋裏面打\u
字樣就會在build的時候報錯"非法的Unicode轉義"。