首页 > 技术文章 > Javac编译原理

tianming1223 2019-01-04 14:51 原文

Javac是什么

Javac是一种编译器,能将一种语言规范转化成为另一种语言规范。

Javac的工作流程

 

1、词法分析

读取源代码,一个字节一个字节的读取,找出其中我们定义好的关键字(如java中的if、else、for、while等关键词,识别哪些if是合法的关键词,哪些不是),这就是词法分析器进行词法分析的过程,其结果是从源代码中找出规范化的Token流。

2、语法分析

通过语法分析器对词法分析后Token流进行语法分析,这一步检查这些关键字组合再一次是否符合java语言规范(如在if后面是不是紧跟着一个布尔判断表达式),词法分析的结果是形成一个符合java语言规范的抽象语法树。

3、语义分析

通过语义分析器进行语义分析。语音分析主要是将一些难懂的、复杂的语法转化成更加简单的语法,结果形成最简单的语法(如将foreach转换成for循环 ,好有注解等),最后形成一个注解过后的抽象语法树,这个语法树更为接近目标语言的语法规则。

4、生成字节码

通过字节码生产器生成字节码,根据经过注解的语法抽象树生成字节码,也就是将一个数据结构转化为另一个数据结构。

Javac的工作原理

1.词法分析

Javac的主要词法分析器的接口是com.sun.tools.javac.parser.Lexer,它的默认实现类是com.sun.tools.javac.parser.Scanner,Scanner会逐个读取Java源文件的单个字符,然后解析出符合Java语言规范的Token序列。

词法分析过程是在JavacParser的parsrCompilationUnit的方法中完成的,从这个方法的代码中可以看出Javac分析词法的原貌,从源文件的一个字符开始,按照Java语言规范一依次找出package、import、类定义,一级属性和方法定义等,最后构建为对应的Token流。

例如:

package compile;

public class Cifa {

int a;
int c=a+1;
}

此类的Token为:  

Token.PACKAGE(Name:package)=>Token.IDENTIFIER(Name:compile)=>Token.SEMI(Name:;)=>Token.PUBLIC(Name:public)=>Token.CLASS(Name:class)=>Token.IDENTIFIER(Name:Cifa)=>Token.LBRACE(Name{)=>Token.INT(Name:int)=>Token.IDENTIFIER(Name:a)=>Token.SEMI(Name:;)=>Token.INT(Name:int)=>Token.IDENTIFIER(Name:b)=>Token.EQ(Name:=)=>Token.IDENTIFIER(Name:a)=>Token.PLUS(Name:+)=>Token.IDENTIFIER(Name:1)=>Token.SEMI(Name:;)=>Token.RBRACE(Name:})

Javac是如何分辨出一个个的Token

Javac进行词法分析时会根据java语言规范来控制什么顺序,在什么地方应该出现什么Token(例如对package的读取:在创建javacParsepackage对象的构造函数时,Scanner会读取第一个Token(Token.PACKAGE),而词法分析器的整个过程是在javacParser的parsrCompilationUnit方法中完成的,先判断当前的Token是不是Token.PACKAGE(是的话读取package的定义),接着读取下一个Token(IDENTIFIER),再读取类名时如果遇到Token.Dot也就是‘.’将继续往下读,直到读得完成类名即遇到Token.SEMI(“;”)为止)。由此可以看出,读取哪个Token是由javacParser规定的而Token流的顺序要符合java语言规范。

Javac如何确认一个Token的

如何确定字符组合是一个Token的规则是在Scanner的nextToken方法中定义的,每调用该方法一次就会构造一个Token,而这些Token必然是com.sun.tools.javac.parser.Token中的任何元素之一。

在读取每个Token时都需要一个转换过程(如在package中的"compile"包名要转化成Token.IDENTIFIER类型),在Java源码中的所有字符集合都要找到在com.sun.tools.javac.parser.Token中定义的对应关系,这个任务是在com.sun.tools.javac.parser.Keywords类中完成的,Keywords负责将所有字符集合对应到Token流中

2.语法分析器

语法分析器就是将Token流组装成更加结构化的语法树,也就是将一个个的单词组装成语法树

 每个语法树上的语法节点都是com.sun.tools.javac.tree.JCTree的实例,语法树的一些规则如下:

1.每个语法节点都会实现一个xxxTree接口,该接口继承自com.sun.source.tree.Tree接口。如IfTree语法节点表示一个if类型表达式
2.每个语法节点都是com.sun.tools.javac.tree.JCTree的子类并且会实现1中提及的接口类,这个类的类名类似于JCxxx类,如实现IfTree接口的实现类为JCIf
3.所有的JCxxx类都作为一个静态内部类定义在JCTree类中

JCTree类中有如下三个重要的属性

1.Tree tag:每个节点都会用一个整形常数表示,并且每个节点的类型的数值是前一个节点的类型数值加1
2.pos:也是一个整数,表示语法节点在源文件中的起始位置,文件的起始位置为0,-1的话表示不存在
3.type:表示这个语法节点是什么类型,如int、float还是String

语法树解析

package解析:JCIdent=>JCFieldAccess

根据Name对象构建了一个JCIdent语法节点,如果是多级目录,将构建JCFieldAccess语法节点,JCFieldAccess语法节点可以是嵌套关系

import解析:JCIdent=>JCFieldAccess=>JCFieldAccess=>JCIdent

当成功解析出package语法节点之后,parseCompilationUnit()这个节点会调用importDeclaration()方法来解析得到import语法树。

importDeclaration()首先检查Token是不是Token.IMPORT,如果是则构造一个语法树,再匹配是不是有static关键字看看是不是静态引入。接着importDeclaration()就会调用语法解析器的Ident()方法解析出一个JCIdent语法节点,如果import语句中包含多级目录的时候,语法解析器就会调用Select()方法解析为嵌套的JCFieldAccess语法节点。

当语法解析器成功解析出JCIdent和JCFieldAccess节点之后,importDeclaration()方法会调用Import()方法,将之前解析的语法节点,整合成为一棵JCImport节点。

实际开发中,通常会有多个import关键字声明,那么importDeclaration()方法内部会通过迭代循环方法解析出多个JCImport语法树,然后将其存储在一个集合中。

class解析:

Import节点解析完之后就是累的解析(interface、class、enum),以class为例

package compile;

public class Yufa {

int a;
private int c=a+1;

public int getC(){

  return c;

}

public void setC(int c){

  this.c=c;

}

}

第一个Token是Token.CLASS这个类的关键词,接下来是用户定义Token.IDENTIFIER,也是类名。然后是参数,下一个是Token.EXTENDS或者Token.IMPLEMENTS,接着是对classbody的解析,这个classbody解析的结果保存在list集合中,最后将这些子节点添加到JCClassDecl这颗class树中。

这个类解析完成之后,会把这些子树加到顶层语法节点JCCompilationUnit(以package作为pid并且持有JCClassDecl语法节点的集合)之下,完整的语法树如下:

 

 3.语义分析器

1)主要由com.sun.tools.javac.comp.Enter类实现将java类中的符号输入到符号表中:第1步将所有类中出现的符号输入到自身的符号表,并将类符号、类的参数类型符号(泛型参数类型)、超类符号和继承的接口类型符号都存储到一个未处理列表中。第二步将这个未处理列表中的所有类都解析到各自的类符号列表中

2)增加构造函数,如前面介绍的Yufa.java,经过Enter类解析后源码变为:

package compile;

public class Yufa {

public Yufa{

supeer();

}

int a;
private int c=a+1;

public int getC(){

  return c;

}

public void setC(int c){

  this.c=c;

}

}

3)由com.sun.tools.javac.processing.JavacProcessingEnvironment类处理注解

4)由com.sun.tools.javac.comp.Attr(需要其他类协助)来检查语义的合法性并进行逻辑判断,主要包括变量的类型是否匹配、变量在使用前是否初始化、能够推导出泛型方法的参数类型、字符串常量的合并

public class Yufa {

int a=0;

private int c=a+1;

private int d=1+1;

private String s="hello"+"word";
}

经过Attr解析后这个源码变为

public class Yufa {

public Yufa{

supeer();

}

int a=0;

private int c=a+1;

private int d=1+1;

private String s="helloword";
}

5)由com.sun.tools.javac.comp.Flow类完成数据流分析,具体工作为检查变量使用前是否正确赋值、final变量是否不会被重复赋值、方法的返回值类型是否确定、检查异常是否已捕获或向上抛出、是否存在不会被执行的语句

6)进一步对语法树进行语义分析,去掉无用的代码(如永远为false的判断)、变量的自动转换(如将int自动包装成Integer类型)、解除语法糖(如foreach改为标准for循环)

去掉无用的代码,如;

public class Yuyi{

public static void main(String[] args){

if(false){

System.out.println("if");
}else{

System.out.println("else");

}

}

}

经过Flow后就变成了

public class Yuyi{

public Yuyi(){

super();

}

public static void main(String[] args){

  {

  System.out.println("else");

  }

}

}

 

变量的自动转换,如

public class Yuyi{

public static void main(String[] args){

Integer i =1;

Long l = i+2L;

System.out.println(l);

}
}

经过自动转化后的代码如下

public class Yuyi{

public Yuyi(){

super();

}

public static void main(String[] args){

Integer i = Integer.valueOf(1);

Long l = Long.valueOf(i.intValue()+2L);

System.out.println(l);

}

}

 

foreach转化成为for循环的例子

public class Yuyi{

public static void main(String[] args){

  int[] array = {1,2,3};

  for (int i:array){

    System.out.println(i);

  }

}

}

解除语法糖后的代码如下所示

public class Yuyi{

public Yuyi(){

super();

}

public static void main (String[] args){

int[] array = {1,2,3};

for(int[] arr$=array,len$=arr$.length.i$=0;i$<len$;++i$){

 

int i = arr$[i$];

{

  System.out.println(i);

}

}

}

}

4.代码生成器

生成Java方法中的代码需要经过以下两个步骤:
1)将Java方法中的代码块转成符合JVM语法的命令形式,jvm的所有操作都是基于栈的,所有操作都必须经过出栈和进栈来完成
2)按照jvm的文件组织格式将字节码输出到以class文扩展名的文件中

 

 

 

推荐阅读