编译器分类
前端编译器:javac之类,将java代码编译成字节码
即时编译器:Hotspot的C1、C2等,运行期间把字节码编译成本地代码
提前编译器:直接把java代码编译成字节码。
javac编译过程
(1)准备过程:初始化插入式注解处理器
(2)解析与填充符号表过程。包括:
- 词法解析:将源码中的字符流转换为标记集合的过程。
- 语法解析:根据标记序列构造出抽象语法树的过程,它的每个节点都代表一个语法结构。
- 填充符号表:符号表是由一组符号地址和符号信息构成的数据结构,它所登记的内容用于语义检查和产生中间代码,在目标代码生成阶段,对符号进行地址分配时,符号表是地址分配的直接依据。
(3)插入式注解处理器的注解处理过程:插入式注解处理器的执行阶段。
- 允许读取、修改、添加抽象语法树中的任意元素。如果在处理注解期间对语法树进行过修改,编译器将回到解析和填充符号表的过程重新处理。
- 著名的Lombok就是通过注解来实现自动生产getter/setter方法、进行空置检查、生成受查异常表、产生equals()和hashCode()等。
(4)语义分析与字节码生成过程。包括:
- 标注检查。对语法的静态信息进行检查。包括变量使用前是否被声明、变量与赋值之间的数据类型是否能够匹配等。它还有一个常量折叠的一个优化,如a=1+2会优化成a=3。
- 数据流及控制流分析。它是对程序上下问逻辑更进一步的验证,它可以检查出诸如程序局部变量在使用前是否赋值、方法的每条路径是否有返回值、是否所有的受查异常读被正确处理等。如方法中的final属性就是在该阶段进行检查,因为局部变量的标志不会被Class文件保存,加载的验证阶段无法进行检查。
- 解语法糖。将简化代码编写的语法糖还原成原有的形式。如泛型、自动拆箱装箱、变长参数等
- 字节码生成。将前面各个步骤所生成的信息转化成字节码。编译器还进行了少量的代码添加和转换工作,如实例构造器<init>和类构造器<cinit>方法都是在该阶段添加的。编译器会把变量初始化、语句块等操作收敛到构造方法中(<init>方法调用父类的实例构造器,<cinit>则不用调用父类的类构造器,因为虚拟机会保证其父类会加载进入)。
注意:第三阶段可能会产生新的符号,如果有新的符号产生,就必须转回第二阶段重新处理新的符号。
泛型
- 泛型的本质是参数化类型或参数化多态的应用,即将其操作的数据类型指定为方法签名中的一种特殊参数,这种参数类型能够用在类、接口和方法的创建中,分别构成泛型类、泛型接口和泛型方法。
- java泛型的实现方式叫做“类型擦除式泛型”,也就是在编译后的字节码文件中,全部泛型都被替换为原来的裸类型,并在相应的地方插入了强制转型代码,如ArrayList<Integer>与ArrayLIst<String>其实是同一种类型。
- 在编译时把ArrayList<Integer>还原回ArrayList,只在元素访问、修改时自动插入一些强制类型转换和检查命令。因为基本类型无法转换成Object类,所以不能用基本类型而是使用其对应的包装类。
下面的类是无法通过编译的,因为List<String>和List<Integer>会被擦除变回List,导致这两个的方法特征签名一样。
public static void method(List<String> list){
}
public static void method(List<Ingeter> list){
}
条件编译
只有使用条件为常量的if语句才会进行条件编译
public static void main(String[] args){
if(true){
System.out.println("block1");
} else {
System.out.println("block2");
}
)
该代码编译的字节码文件反编译后:
public static void main(String[] args){
System.out.println("block1");
}
可以看到编译器会把分支中不成立的代码快消除掉。如果使用常量与其他带有条件判断能力的语句搭配,则可能在控制流分析中提示错误拒绝编译。如:
public static void main(String[] args){
while (false) {
System.out.println("");
}
}