第10章 前端编译与优化
10.1 概述
- 在Java技术下谈“编译期”而没有具体上下文语境的话,其实是一句很含糊的表述,因为它可能是指一个前端编译器(叫“编译器的前端”更准确一些)把*.java文件转变成 *.class文件的过程;也可能是指Java虚拟机的即时编译器(常称JIT编译器)运行期把字节码转变成本地机器码的过程;还可能是指使用静态的提前编译器(常称AOT编译器)直接把程序编译成与目标机器指令集相关的二进制代码的过程。
- 语法糖:也译为糖衣语法,是由英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。
- JAVA属于一种“低糖语法”的语言(相对C#及其他JVM语言),使用JAVA语言编程的过程中语法糖随处可见,如:泛型、自动包装、自动拆箱、内部类、变长参数等,在虚拟机运行时并不支持这些语法,所以在编译器会把这些语法编译成JVM运行时能读懂的class字节码,这个过程叫:解语法糖,接下来我们来分析语法糖在JAVA中的使用。
- javac这类前端编译器对代码的运行效率几乎没有任何优化措施可言,哪怕是编译器真的采取了优化措施也不会产生什么实质的效果。
10.2 Javac编译器
- Javac编译器是一个由Java语言编写的程序
- Javac的编译过程大致可分为1个准备过程和3个处理过程。其中准备过程:初始化插入式注解处理器。
10.2.2 解析与填充符号表
- 词法,语法分析
词法分析是将源代码的字符流转变为标记(Token)集合的过程,单个字符是程序编写时的最小元素,但标记才是编译时的最小元素。关键字,变量名,字面量。运算符都可以作为标记。
语法分析是根据标记序列构造抽象语法树的过程,抽象语法树的每一个节点都代表着程序代码中的一个语法结构,例如包,类型,修饰符,运算符,接口,返回值甚至连代码注释等都可以是一种特定的语法结构。
经过词法和语法分析生成语法树以后,编译器就不会再对源码字符流进行操作了,后续的操作都建立在抽象语法树上。 - 填充符号表
完成了语法分析和词法分析,下一个阶段是对符号表进行填充的过程。符号表是由一组符号地址和符号信息构成的数据结构。
10.2.3 注解处理器
- 程序员能使用插入式注解处理器来实现许多原本只能在编码中由人工完成的事情。譬如Java著名的编码效率工具Lombok
10.2.4 语义分析和字节码生成
- 抽象语法树能够表示一个结构正确的源程序,但无法保证源程序的语义是符合逻辑的。而语义分析的主要任务则是对结构上正确的源程序进行上下文相关性质的检查,譬如进行类型检查,控制流检查,数据流检查,等等。
-
标注检查
- Javac在编译过程中,语义分析过程可分为标注检查和数据及控制流分析两个步骤。
- 标注检查中还会顺便进行一个称为常量折叠的代码优化,这是Javac编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)
-
数据及控制流分析
-
解语法糖
- 语法糖,也称糖衣语法,指的是在计算机语言中添加的某种语法,这种语法对语言的编译结果和功能并没有实际影响,但是却能更方便程序员使用该语言。通常来说使用语法糖能够减少代码量,增加程序的可读性,从而减少程序代码出错的机会。
- Java中最常见的语法糖包括了前面提到过的泛型,变长参数,自动装箱拆箱等等。Java虚拟机运行时并不直接支持这些语法,它们在编译阶段被还原回原始的基础语法结构,这个过程就称为解语法糖。
-
字节码生成
- 字节码生成是Javac编译过程的最后一个阶段。
10.3 Java语法糖的味道
- Java选择的泛型实现方式叫做“类型擦除式泛型”,而C#选择的泛型实现方式是“具现化式泛型”
类型擦除
- 裸类型(Rae Type),裸类型应该被视为所有该类型泛型化实例的共同父类型(Super Type)
- 方法重载要求方法具备不同的特征签名,返回值并不包含在方法的特征签名中,所以返回值不参与重载选择,但是在Class文件格式之中,只要描述符不是完全一致的两个方法就可以共存。也就是说两个方法如果有相同的名称和特征签名,但返回值不同,那他们也是可以合法地共存于一个Class文件中的。
- Signature是其中最重要的一项属性,它的作用就是存储一个方法在字节码层面的特征签名,这个属性中保存的参数类型并不是原生类型,而是包括了参数化类型的信息
- 在Java代码中的方法特征签名只包括了方法名称,参数顺序及参数类型,而在字节码中的特征签名还包括方法返回值及受查异常表。
10.3.2 自动装箱,拆箱与遍历循环
- 遍历循环将代码还原成迭代器的实现,这也是为何遍历循环需要被遍历的类实现Iterable接口的原因。
- 包装类的“==”运算在不遇到算术运算的情况下不会自动拆箱以及它们equals()方法不处理数据转型的关系。
10.3.3 条件编译
- —般情况下,C语言源程序中的每一行代码.都要参加编译。但有时候出于对程序代码优化的考虑.希望只对其中一部分内容进行编译.此时就需要在程序中加上条件,让编译器只对满足条件的代码进行编译,将不满足条件的代码舍弃,这就是条件编译(conditional compile)。
- Java语言进行条件编译的方法就是使用条件为常量的if语句。