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.语法分析器
每个语法树上的语法节点都是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.代码生成器
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步