第10章_前端编译与优化

概述

编译期可能指以下几种:

  1. 前端编译器:把java文件转为class文件的过程,如Javac
  2. 即时编译器(JIT编译器):运行期把字节码转变为本地机器码的过程,如hotspot虚拟机的C1,C2编译器
  3. 静态的提前编译器:直接把程序编译成与目标指令集相关的二进制代码的过程

Java中,即时编译器在运行期的优化过程,支撑了程序执行效率的不断提升,而前端编译器在编译期的优化过程,则是支撑着程序员的编码效率和语言使用者的幸福感的提高

Javac编译器

编译过程

  1. 准备过程:初始化插入式注解处理器
  2. 解析与填充符号表过程
    • 词法,语法分析,构造出抽象语法树
    • 填充符号表,产生符号地址和符号信息
  3. 插入式注解处理器的注解处理过程:插入式注解处理器的执行阶段
  4. 语义分析与字节码生成过程
    • 标注检查.对语法的静态信息进行检查
    • 数据流及控制流分析。对程序动态运行过程进行检查
    • 解语法糖。将简化代码编写的语法糖还原为原有的形式
    • 字节码生成。将前面各个步骤所生成的信息转换成字节码

上述动作中,执行插入式注解时有可能会产生新的符号,如果有新符号产生,就必须回到之前的解析,填充符号表的过程中重新处理这些新符号

解析与填充符号表

词法,语法分析

词法分析是将源代码的字符流转变为标记集合的过程,标记是编译时的最小元素,例如int就是一个标记

语法分析是根据标记序列构造抽象语法树的过程,抽象语法树是一种用来描述程序代码语法结构的树形表示方式

填充符号表

符号表是一组符号地址和符号信息构成的数据结构,可以当作以键值对存储的哈希表

注解处理器

插入式注解处理器:注解一般是在运行期起作用,而这提前到编译期对代码中的特定注解进行处理,从而影响到前端编译器的工作过程。

可以把插入式注解处理器看作是编译器的插件,如果这些插件在处理注解期间对语法树进行过修改,编译器将回到解析及填充符号表的过程重新处理,直到所有的插入式注解处理器都没有再对语法树进行修改为止,每一次循环过程称为轮次。

插入式注解处理器可以实现很多功能,如通过注解来实现自动产生getter/setter方法,产生equals(),hashCode()方法

语义分析与字节码生成

抽象语法树能表示一个结构正确的源程序,但无法保证源程序的语义符合逻辑。语义分析主要任务是对结构正确的源程序进行上下文相关性质的检查,例如类型检查等

编译时在IDE中看到由红线标注的错误提示,其中绝大部分都是由语义分析阶段的检查结果

标注检查

检查变量使用前是否被声明,变量与赋值之间的数据类型是否匹配等

常量折叠:int a=1+2,会变成 a=3

数据及控制流分析

检查出诸如程序局部变量在使用前是否有赋值,方法的每条路径是否都有返回值,是否所有的受查异常都被正确处理了等问题

解语法糖

在语言中添加某种语法,对语言的编译结果和功能没有实际影响,但能方便程序员使用该语言。减少代码量,增加程序可读性,减少程序代码出错的机会

字节码生成

实例构造器()方法和类构造器(),就是这个阶段添加到语法树的

(),()的产生是代码收敛的过程,编译器会把语句块,变量初始化,调用父类的实例构造器等操作收敛到他们两个方法上

并且保证无论源码中出现的顺序如何,都一定是按先执行父类的实例构造器,然后初始化变量,最后执行语句块的顺序执行

Java语法糖的味道

泛型

泛型本质是参数化类型或参数化多态的应用。即可以将操作的数据类型指定为方法签名中的一种特殊参数

Java的泛型是“类型擦除式泛型”,只在程序源码中出现,在编译后的字节码文件中,全部泛型都被替换为原来的裸类型(Raw Type),并且在相应的地方插入了强制转型代码

所以ArrayList,ArrayList 在运行期是同一种类型

无论在使用效果还是运行效率上,都全面落后于具现化式泛型,唯一优点就是实现擦除式泛型只需要在Java编译器上改进即可…

类型擦除

Java选择把已有的类型泛型化,如ArrayList原地泛型化为ArrayList
裸类型应被视为所有该类型泛型化实例的共同父类型
让所有泛型化的实例类型,如ArrayList,ArrayList 都自动成为ArrayList的子类型
Java裸类型的实现:简单粗暴地直接在编译期把ArrayList还原为ArrayList,只在元素访问,修改时自动插入一些强制类型转换和检查指令

不过擦除法所谓的擦除,仅仅是对方法的Code属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息。所以编码时能通过反射手段取得参数化类型

问题

对原始类型数据不支持

因为不支持int这些基本类型与Object之间的强制转型,一旦把泛型信息擦除后,到要插入强制转型代码地方就不能进行下去。Java就不支持原生类型的泛型,只能用ArrayList,导致了无数构造包装类的装箱,拆箱的开销

代码啰嗦

运行期无法取得泛型类型信息

带来了模棱两可的模糊状况

当泛型遇到重载时

自动装箱,拆箱,循环遍历

循环遍历则是把代码还原成迭代器的实现,所以需要被遍历的类实现Iterator接口

变长参数在调用时变成一个数组类型的参数

Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
Long g = 3L;
System.out.println(c==d);//true
System.out.println(e==f);//false
System.out.println(c==(a+b));//true
System.out.println(c.equals(a+b));//true
System.out.println(g==(a+b));//true
System.out.println(g.equals(a+b));//false

鉴于包装类的 == 运算在不遇到算术运算的情况下不会自动拆箱,以及他们的equals()方法不处理数据转型的关系,实际编码中尽量避免这样使用自动装箱,拆箱

条件编译

Java语言编译方式:编译器并非一个个地编译java文件,而是将所有编译单元的语法树顶级节点输入到待处理列表中再进行编译,因此各个文件之间能够互相提供符号信息

Java的条件编译:使用条件为常量的if语句,它在编译期就会运行


public static void main(String[] args)
{
    
    
    if(true)
    {
    
    
        System.out.println("1");
    }
    else
    {
    
    
        System.out.println("2");
    }
}

该代码编译后Class文件的反编译结果:

public static void main(String[] args)
{
    
    
   System.out.println("1");
}

编译器将会把分支中不成立的代码消除掉。

这种语法糖只能写在方法体内部,只能实现语句基本块级别的条件编译,而没有办法实现根据条件调整整个Java类的结构

java
public static void main(String[] args)
{
System.out.println(“1”);
}


编译器将会把分支中不成立的代码消除掉。

这种语法糖只能写在方法体内部,只能实现语句基本块级别的条件编译,而没有办法实现根据条件调整整个Java类的结构

猜你喜欢

转载自blog.csdn.net/weixin_42249196/article/details/108295165