一个编译器的前端把*.java文件转变成*.class文件的过程称为Java前端编译。像Javac这类前端编译器对代码的运行效率几乎没任何优化措施,但是其做了许多针对Java语言编码过程的优化措施来降低程序员的编码复杂度、提供编码效率。
1 Javac编译器
准备过程 | 初始化插入式注解处理器。 |
解析与填充符号表过程 | 词法、语法分析。将源代码的字符流转变为标记集合,构造出抽象语法树。 填充符号表。产生符号地址和符号信息。 |
插入式注解处理器的注解处理过程 | 插入式注解处理器的执行阶段。 |
分析与字节码生成过程 | 标注检查。对语法的静态信息进行检查。 数据流及控制流分析。对程序动态运行过程进行检查。 解语法糖。将简化代码编写的语法糖还原为原有的形式。 字节码生成。将前面各个步骤所生成的信息转化为字节码。 |
表 javac编译过程
上述三个处理过程,执行插入式注解时又可能会产生新的符号,如果有新的符号产生,就必须转回到之前的解析、填充符号表的过程中重新处理这些新符号。
图 Javac的编译过程
Javac编译动作的入口是com.sun.tools.javac.main.JavaCompiler类,上述三个过程的代码逻辑集中在这个类的compile()和compile2()方法。
图 JavaCompiler类的compile()方法源码
图 JavaCompiler类的compile2()方法部分源码
1.1 解析与填充符号表
解析过程包括经典程序编译原理中的词法分析与语法分析两个步骤。
1.1.1 词法、语法分析
词法分析是将源代码的字符流转变为标记(Token)集合的过程。单个字符是程序编写时的最小元素,但标记才是编译时的最小元素。
语法分析是根据标记序列构造抽象语法树的过程,抽象语法树(AST)是一种用来描述程序代码语法结构的树形表示方式,抽象语法树的每一个节点都代表着程序代码中的一个语法结构。
1.1.2 填充符号表
enterTrees()方法,对符号表进行填充。符号表是一组符号地址和符号信息构成的数据结构(类似哈希表中键值对的存储形式)。符号表所登记的信息在编译的不同阶段都要被用到。
填充符号表的过程由com.sum.tools.javac.comp.Enter类实现,该过程的产物是一个待处理列表,其中包含每一个编译单元的抽象语法树的顶级节点,以及package-info.java(如果存在的话)的顶级节点。
图 抽象语法树相关UML
图 JCTree类的组成
1.2 注解处理器
插入式注解处理器的初始化过程是在initProcessAnnotations()方法中完成的。
它的执行过程是在processAnnotations()方法中完成,这个方法会判断是否还有新的注解器需要执行,如果有的话,通过JavacProcessingEnvironment类的doProcessing()方法生成一个新的JavaCompiler对象。
1.3 语义分析与字节码生成
在经过语法分析之后,编译器获得了程序代码的抽象语法树表示,抽象语法树能够表示一个结构正确的原程序,但无法保证源程序的语义是符号逻辑的。
语义分析的主要任务是对结构上正确的源程序进行上下文相关性质的检查,比如进行类型检查、控制流检查、数据流检查。
1.3.1 标注检查
标注检查由attribute()方法完成。标注检查步骤要检查的内容包括诸如变量使用前是否已被声明、变量与赋值直接的数据类型是否能够匹配等。由flow()方法完成。
1.3.2 数据及控制流分析
检查出诸如程序局部变量在使用前是否有赋值、方法的每条路径是否都有返回值、是否所有的受查异常都被正确处理了等问题。
1.3.3 解语法糖
语法糖指的是计算机语言中添加的某种语法,这种语法对语言的编译结果和功能都并没有实际影响,但是却能更方便程序员使用该语言。语法糖能减少代码量、增加程序可读性,从而减少程序代码出错的机会。
Java中最常见的语法糖包括泛型、变长参数、自动装箱拆箱等。解语法糖的过程由desugar()方法触发。
1.3.4 字节码生成
是javac编译过程的最后一个阶段,由Gen类来完成。字节码生成阶段不仅是把前面各个步骤所生成的信息转化成字节码指令写到磁盘中,编译器还进行了少量的代码添加和转换工作。