这节跟着书上第三章学习解析例如{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文件的常量池中,没有长度限制。
这里需要注意语法规则必须小写字母开头,一般直接用全小写。而词法符号规则必须大写字母开头,一般直接用全大写。
这里还有一个技巧,因为生成的程序类名总是这个语法名+后缀拼接的方式,为了符合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就可以了。
这个文件中存放语法分析器类:
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 { ... }
另外还有一些辅助代码。
这个文件中存放词法分析器类:
public class ArrayInitLexer extends Lexer { ... }
它是通过分析.g4
文件中的词法规则(INT
和WS
)以及语法规则中的字面值({
、}
和,
)生成的,用于将输入的字符序列分解成词法符号。
这个文件用于存储词法符号到一个数字值的对应关系,每个词法符号都会对应一个数字值,使用它可以在多个小型语法之间同步全部的词法符号类型,以将一个大型语法切分成多个小型语法。
该文件用于存储监听器接口:
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);
进入/退出哪个结点,就会调用上面相应的方法,并且把那个结点的上下文对象作为参数传进去。
该文件用于存储监听器接口的默认实现类:
public class ArrayInitBaseListener implements ArrayInitListener { ... }
也就是ANTLR提供的ArrayInitListener
接口的默认实现,可以看到里面的回调方法都是空的,只需要按照需求覆盖感兴趣的回调方法,就能用监听器的方式来访问语法分析树了。
这里通过继承前面生成的监听器类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转义"。