Javac编译原理

2016/4/6 posted in  深入分析Java Web技术内幕

如何才能编译程序呢?

  • 首先,要读取源代码,一个字节一个字节地读进来,找出这些字节中哪些是我们定义的语法关键词。
    • 词法分析的结果就是从源码中找到一些规范化的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);
     }
 }