如何才能编译程序呢?
- 首先,要读取源代码,一个字节一个字节地读进来,找出这些字节中哪些是我们定义的语法关键词。
- 词法分析的结果就是从源码中找到一些规范化的Token流,就像人类语言中,给你一句话,你要能分辨出哪些是一个词语,哪些是标点符号
- 接着,就是对这些Token流进行语法分析,检查这些关键词组合在一起是不是符合Java语言规范
- 语法分析的结果就是形成一个符合Java语言规范的抽象语法树
- 接下来是语义分析,判断语义是不是正确。主要工作是把一些难懂的、复杂的语法转化成更加简单的语法。
- 最后,通过字节码生成器生成字节码,将会根据经过注解的抽象语法树生成字节码,就像将所有的中文词语翻译成英文单词后,按照英文语法组装成英文语句。
Javac工作原理分析
词法分析器
public class Cifa {
int a;
int c = a + 1;
}
Javac的主要词法分析器的默认实现类是com.sun.tools.javac.parser.Scanner,Scanner会逐个读取Java源文件的单个字符,然后解析出符合Java语言规范的Token序列。
JavacParser规定了哪些词是符合Java语言规范规定的词;具体读取和归类不同语法的操作由Scanner完成。Token规定了所有Java语言的合法关键词,Names用来存储和表示解析后的词法。
词法分析过程是在JavaParser的parseCompilationUnit方法中完成的。
从源文件的第一个字符开始,按照Java语法规范依次找出package、import、类定义,以及属性和方法定义等,最后构建一个抽象语法树。
词法分析器的分析结果就是将这个类中的所有关键词匹配到Token类的所有项中的任何一项。
语法分析器
词法分析器的作用就是将Java源文件的字符流转变成对应的Token流。而词法分析器是将词法分析器分析的Token流组建成更加结构化的语法树,也就是将一个个单词组装成一句话,一个完整的语句。哪些词语组合在一起是主语、哪些是谓语,要做进一步区分。
Javac的语法树使得Java源码更加结构化,这种结构化可以为后面的进一步处理提供方便。每个语法树上的节点都是com.sun.tools.javac.tree.JCTree
的一个实例
JCTree类中有如下三个重要的属性项:
- Tree tag: 每个语法节点都会用一个整形常数表示,并且每个节点类型的数值是在前一个的基础上加1。顶层节点TOPLEVEL是1,而IMPORT节点等于TOPLEVEL加1,等于2
- pos:也是一个整数,它存储的是这个语法节点在源代码中的起始位置,一个文件的一个位置是0,而-1表示一个不存在的位置
- type:表示的是这个节点是什么Java类型,如int、float等
下面以Yufa类为例,看看它的最后的语法树是什么:
public class Yufa {
int a;
private int c = a + 1;
public int getC() {
return c;
}
public void setC(int c) {
this.c = c;
}
}
这段代码对应的语法树是
语义分析器
前面介绍了将一个Java源文件先解析成一个一个的Token流,然后再经过语法分析器将Token流解析成更加结构化、可操作的一颗语法树。
将Java类中的符号输入到符号表中
主要由com.sun.tools.javac.comp.Enter
类完成,这个类主要完成以下两个步骤。
- 将所有类中出现的符号输入到类本身的符号表中,所有类符号、类的参数类型符号(泛型参数类型)、超类符号和继承的接口类型符号等都存储到一个未处理列表中
- 将这个未处理列表中所有的类都解析到各自的类符号列表中
首先一个类中处理类本身会定义一些符号变量外,如类名称、变量名称和方法名称等,还有一些符号是引用其他类的,符号调用其他类的方法或者变量等,还有一些这个类可能会继承或者实现超类和接口等。这些符号都是在其他类中定义的。
第二个步骤就是按照递归向下的顺序解析语法树,将所有的符号都输入符号表中。
在Enter类解析这一步骤中,还有一个重要的步骤是添加默认的构造函数。
处理annotation
这个步骤是由com.sun.tools.javac.processing.JavaProcessingEnvironment
类完成的。
标注
这个步骤最重要的是检查语义的合法性和进行逻辑判断,如:
- 变量的类型是否匹配
- 变量在使用前是否已经初始化
- 能够推导出泛型方法的参数类型
- 字符串常量的合并
还有一些类来协助
- 检查语法树中的变量类型是否正确,如方法返回的类型是否与接收的引用值类型匹配
- 检查变量、方法或者类的访问是否合法、变量是否是静态变量、变量是否已经初始化
- 常量折叠,这里主要针对字符串常量,会将一个字符串常量中的多个字符串合并成一个字符串
- 帮助推导泛型方法的参数类型等
public class Yufa {
int a = 0;
private int c = a + 1;
private int d = 1 + 1;
private String s = "hello " + "world";
}
经过Attr解析后,这个源码会变成如下:
public class Yufa {
public Yufa() {
super();
}
int a = 0;
private int c = a + 1;
private int d = 1 + 1;
private String s = "hello world";
}
字符串s由两个"hello "和"world"合并成一个字符串
数据流分析
标注完成后就是com.sun.tools.javac.comp.Flow
类完成数据流分析
- 检查变量在使用前是否都已经正确赋值
- 保证final修饰的变量不会被重复赋值
- 方法的返回值类型都要确定
- 所有Checked Exception都要捕获或者向上抛出
- 所有的语句都要被执行到。这里会检查是否有语句出现在一个方法return的后面
进一步度对语法树进行语义分析
语义分析的最后一个步骤是执行com.sun.tools.javac.comp.Flow
,这一步是进一步堆语法树进行语义分析,如消除一些无用的代码,永不真的条件判断将会去除。还有就是解除一些语法糖,如将foreach这种语法解析成标准的for循环形式。
- 去掉无用的代码,如假的if代码块。
- 变量的自动转换,如将int自动包装成Integer类型或者相反的操作等
- 去除语法糖,将foreach的形式转化成更简单的for循环
代码生成器
接下来Javac会调用com.sun.tools.javac.jvm.Gen
类遍历语法树生成最终的Java字节码
生成Java字节码需要经过两个步骤:
- 将Java方法中的代码块转成符合JVM语法的命令形式,JVM的操作都是基于栈的,所有的操作都必须经过出栈和进栈来完成
- 按照JVM的文件组织格式将字节码输出到以class为扩展名的文件中
public class Daima {
public static void main(String[] args) {
int rt = add(1, 2);
}
public static int add(Integer a, Integer b) {
return a + b;
}
}
这段代码调用一个函数,这个函数的作用就是将两个int类型的参数相加,然后将相加的结果返回给调用者。
加法表达式:必须先将两个操作数a和b放到操作栈,然后再利用加法操作符执行加法操作,将加法的结果放到当前栈的栈顶,最后将这个结果返回给调用者。
这个过程可以用如下方式描述:
- 先计算左表达式结果,将左表达式结果转化成int类型
- 将这个结果放入当前栈中
- 再计算右表达式结果,将右表达式结果转化成int类型
- 将这个结果放入当前栈中
- 弹出‘+’操作符
- 将操作结果置于当前栈栈顶
设计模式解析之访问者模式
访问者模式的结构
这种模式的基本想法如下:
- 首先我们拥有一个由许多对象构成的对象结构,这些对象的类都拥有一个accept方法用来接受访问者对象
- 访问者是一个接口,它拥有一个visit方法,这个方法对访问到的对象结构中不同类型的元素作出不同的反应
- 在对象结构的一次访问过程中,我们遍历整个对象结构,对每一个元素都实施accept方法,在每一个元素的accept方法中回调访问者的visiti方法,从而使访问者得以处理对象结构的每一个元素。
- 我们可以针对对象结构设计不同的实在的访问者类完成不同的操作。
- 抽象访问者(Visitor):声明所有访问者需要的接口
- 具体访问者(Concrete Visitor):实现抽象访问者声明的接口
- 抽象节点元素(Element):提供一个接口能够接受访问者作为参数传递给节点元素
- 具体节点元素(ConcreteElement):实现抽象节点元素声明的接口
- 结构对象(Object Structure):提供一个接口能够访问到所有的节点元素,一般作为一个集合特有节点元素的引用
- 客户端(Client):分别创建访问者和节点元素的对象,调用访问者访问变量节点元素
interface Visitor {
void visit(Wheel wheel);
void visit(Engine engine);
void visit(Body body);
void visit(Car car);
}
class Wheel {
private String name;
Wheel(String name) {
this.name = name;
}
String getName() {
return this.name;
}
void accept(Visitor visitor) {
visitor.visit(this);
}
}
class Engine {
void accept(Visitor visitor) {
visitor.visit(this);
}
}
class Body {
void accept(Visitor visitor) {
visitor.visit(this);
}
}
class Car {
private Engine engine = new Engine();
private Body body = new Body();
private Wheel[] wheels
= { new Wheel("front left"), new Wheel("front right"),
new Wheel("back left") , new Wheel("back right") };
void accept(Visitor visitor) {
visitor.visit(this);
engine.accept(visitor);
body.accept(visitor);
for (int i = 0; i < wheels.length; ++ i)
wheels[i].accept(visitor);
}
}
class PrintVisitor implements Visitor {
public void visit(Wheel wheel) {
System.out.println("Visiting " + wheel.getName()
+ " wheel");
}
public void visit(Engine engine) {
System.out.println("Visiting engine");
}
public void visit(Body body) {
System.out.println("Visiting body");
}
public void visit(Car car) {
System.out.println("Visiting car");
}
}
public class VisitorDemo {
static public void main(String[] args) {
Car car = new Car();
Visitor visitor = new PrintVisitor();
car.accept(visitor);
}
}