一、说明
阅读完<Hive-技术补充-初识ANTLR>和<Hive-技术补充-ANTLR词法语法分析>后我们对ANTLR已经有了一个初步认识,下面我们就来做一个简单的小项目来感受下它的整个过程。
static short [] data = {1,2,3} ;
小项目目标是把上面的代码转换成下面的Unicode字符
static String data = "\u0001\u0002\u0003" ;
Unicode是一种用于字符编码的国际标准,它定义了世界上所有字符的唯一编号。字符集中的每一个字符分配一个唯一的数字,Unicode使用4个16进制数字来表示一个16位的字符。
我们先编写下上面代码,并查看下java编译器将它编译成class的样子
vi Test.java
java
public class Test{
static short [] data = {1,2,3} ;
}
javac Test.java
javap -c Test
字节码说明:
public Test();
Code:
0: aload_0 //将局部变量表中第 0 个位置的引用类型(对象)压入操作数栈顶
1: invokespecial #1 // Method java/lang/Object."<init>":()V //调用类的私有方法,构造函数初始化方法以及超类方法。
4: return
static {};
Code:
0: iconst_3 //将整型 3 推送至栈顶
1: newarray short //分配数据成员类型为 short 基本数据类型的新数组
3: dup //复制栈顶的元素然后压入栈
4: iconst_0 //将整型 0 推送至栈顶
5: iconst_1 //将整型 1 推送至栈顶
6: sastore //用于将 short 值存储到数组中。它是将一个 short 值存储到局部变量表中指定的 short 数组的索引位置
7: dup //复制栈顶的元素然后压入栈
8: iconst_1 //将整型 1 推送至栈顶
9: iconst_2 //将整型 2 推送至栈顶
10: sastore //用于将 short 值存储到数组中。它是将一个 short 值存储到局部变量表中指定的 short 数组的索引位置
11: dup //复制栈顶的元素然后压入栈
12: iconst_2 //将整型 2 推送至栈顶
13: iconst_3 //将整型 3 推送至栈顶
14: sastore //用于将 short 值存储到数组中。它是将一个 short 值存储到局部变量表中指定的 short 数组的索引位置
15: putstatic #2 // Field data:[S 设置类中静态字段的值
18: return
}
可以发现编译后的字节码已经限制了数组长度为 3 ,如果是一个字符串,java编译器会把它存储为连续的short序列从而不受长度约束。
将数组的初始化语句修改成字符串可以得到更紧凑的class文件,避免了java对初始化方法的长度限制。
下面我们通过ANTLR来实现它。
二、ANTLR目录结构
我们用jd-gui打开下载的 ANTLR jar包(<Hive-技术补充-初识ANTLR>有下载方式)
ANTLR工具是用 org.antlr.v4.Tool 来处理编写好的语法文件,并生成一些代码(词法分析器、语法分析器)。词法分析器将输入的字符流分解为词法符号序列,然后将它们传递给能进行语法检查的语法分析器。
ANTLR运行库是由若干类和方法组成的库,这些类和方法是自动生成的代码(如:Parser、Lexer和Token)运行所必须的。
因此我们完成这个小项目的步骤为:
1、创建语法(即合法语句结构的集合)
2、对语法文件运行ANTLR
3、将生成的代码与jar包中的运行库一起编译
4、运行
三、编写语法文件
vi ArrayInit.g4
//语法通常以 grammar 关键字开头
//这是一个名为ArrayInit的语法,它必须与文件名 ArrayInit.g4 相匹配
grammar ArrayInit;
//一条名为 init 的规则 ,它包括一对花括号中的、逗号分割的value
//必须至少匹配一个value
init : '{' value (',' value)* '}';
//一个value可以是嵌套的花括号结构,也可以是一个简单的整数,即 INT 词法符号
value :init
| INT
;
//语法分析器的规则必须以小写字母开头,词法分析器的规则必须用大写字母开头
INT : [0-9]+ ; //定义词法符号 INT ,它由一个或多个数字组成
WS : [ \t\r\n]+ -> skip ; //定义词法规则 '空白符号' 丢弃
四、对语法文件运行ANTLR
antlr4 ArrayInit.g4
ANTLR为我们自动生成了这些文件,正常情况下这些都需要我们自己编写的,下面看看这些文件的作用。
1、ArrayInitParser.java
该文件包含一个语法分析器类的定义,这个语法分析器专门用来识别我们的目标语言,也就是上面声明的数组。该类每条规则都有对应的方法,还有一些辅助代码。
2、ArrayInitLexer.java
该文件包含一个词法分析器类的定义,ANTLR根据语法文件中词法规则 INT 和 WS ,以及语法中的字面值 '{' ',' '}' 生成。
3、ArrayInit.tokens
ANTLR会给我们定义的词法符号指定一个数字形式的类型,然后将它们的对应关系存储到此文件。有时我们需要将一个大型语法切割成很多小语法,此时,该文件就非常有用了。通过它,ANTLR可以在多个小语法文件中同步全部的词法类型。
4、ArrayInitListener.java
默认情况下,ANTLR生成的语法分析器可以将输入文本转换成一棵语法分析树。在遍历语法分析树时,遍历器能够触发一系列事件,并通知我们提供的监听器对象。ArrayInitListener给出了这些回调方法的定义,我们可以实现它来自定义功能。ArrayInitBaseListener是对它的默认实现,其中的方法都是空的,我们直接修改即可。
此外,通过指定 -visitor 命令行参数,ANTLR可以为我们生成语法分析树的访问器,通过访问者模式来遍历语法分析树。
接下来,我们将使用监听器将short数组初始化语句转换为字符串对象。
五、编译
javac *.java
六、测试
grun ArrayInit init -tokens
我们输入 {99,3,567}
按Ctrl + D
每行输出代表一个词法符号
也可以使用 -tree 参数来查看语法分析树
grun ArrayInit init -tree
也可以使用 -gui 参数来生成一个可视化的对话框,我们用对话框来处理下嵌套结构(用crt远程连接弹不出对话框,需要在本机桌面才可以)
grun ArrayInit init -gui
七、java程序集成语法分析器
vi TestArrayInit.java
java
//导入ANTLR的运行库
import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.tree.*;
public class TestArrayInit {
public static void main(String [] args) throws Exception{
//新建一个 CharStream,从标准输入读取数据
ANTLRInputStream input = new ANTLRInputStream(System.in);
//新建一个词法分析器,处理输入的 CharStream
ArrayInitLexer lexer = new ArrayInitLexer(input);
//新建一个词法符号缓冲区,用于存储词法分析器将生成的词法符号
CommonTokenStream tokens = new CommonTokenStream(lexer);
//新建一个语法分析器,处理词法符号缓冲区中的内容
ArrayInitParser parser = new ArrayInitParser(tokens);
//针对init语法开始语法分析
ParseTree tree = parser.init();
//用LISP风格打印生成的语法分析树
System.out.println(tree.toStringTree(parser));
}
}
编译并运行
javac ArrayInit*.java TestArrayInit.java
java TestArrayInit
八、实现小项目
我们继承ArrayInitBaseListener来实现自己的监听器,覆盖其中部分方法,在遍历器遍历语法分析树时,调用我们自己写的监听器来实现字符的处理。
我们先来手动的走下这个过程,输入数据为:
short 数组:{99,3,451}
翻译后的结果为:
String形式:"\u0063 \u0003 \u01c3"
{ ----> "
数字 ----> 4位的16进制形式并加前缀 \u
} -----> "
下面我们编写监听类
vi ShortToUnicodeString.java
java
//将类似{1,2,3}的short数组初始化语句翻译为"\u0001\u0002\u0003"
public class ShortToUnicodeString extends ArrayInitBaseListener {
//{ ----> "
@Override
public void enterInit(ArrayInitParser.InitContext ctx) {
System.out.print('"');
}
//} ----> "
@Override
public void exitInit(ArrayInitParser.InitContext ctx) {
System.out.print('"');
}
//整数 ----> 前缀\u + 4位16进制形式
@Override
public void enterValue(ArrayInitParser.ValueContext ctx) {
//先假定不存在嵌套结构
int value = Integer.valueOf(ctx.INT().getText());
System.out.printf("\\u%04x",value);
}
}
vi Translate.java
java
//导入ANTLR的运行库
import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.tree.*;
public class Translate {
public static void main(String [] args) throws Exception{
//新建一个 CharStream,从标准输入读取数据
ANTLRInputStream input = new ANTLRInputStream(System.in);
//新建一个词法分析器,处理输入的 CharStream
ArrayInitLexer lexer = new ArrayInitLexer(input);
//新建一个词法符号缓冲区,用于存储词法分析器将生成的词法符号
CommonTokenStream tokens = new CommonTokenStream(lexer);
//新建一个语法分析器,处理词法符号缓冲区中的内容
ArrayInitParser parser = new ArrayInitParser(tokens);
//针对init语法开始语法分析
ParseTree tree = parser.init();
//新建一个通用的,能够触发回调函数的语法分析树遍历器
ParseTreeWalker walker = new ParseTreeWalker();
//遍历语法分析过程中生成的语法分析树,触发回调
walker.walk(new ShortToUnicodeString(),tree);
//用LISP风格打印生成的语法分析树
System.out.println();
}
}
编译测试
javac *.java
java Translate
输入{99,3,451}