Javac 编译器

Posted cd_along

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Javac 编译器相关的知识,希望对你有一定的参考价值。

编译过程

Javac 编译过程大致可以分为1个准备过程和3个处理过程:

  1. 准备过程:初始化插入式注解处理器。
  2. 解析与填充符号表过程,包括:
    1. 词法、语法分析,将源代码的字符流转变为标记集合,构造出抽象语法树。
    2. 填充符号表,产生符号地址和符号信息。
  3. 插入式注解处理器的注解处理过程。可以把插入式注解处理器看作是一组编译器的插件,当这些插件工作时,允许读取、修改、添加抽象语法树中的任意元素。如Lombok注解。
  4. 分析与字节码生成过程,包括:
    1. 标注检查。对语法的静态信息进行检查。
    2. 数据流及控制流分析。对程序动态运行过程进行检查。
    3. 解语法糖。将简化代码编写的语法糖还原为原有的形式。
    4. 字节码生成。将前面各个步骤所生成的信息转化成字节码。

上述3个处理过程里,执行插入式注解时又可能会产生新的符号,如果有新的符号产生,就必须转回到之前的解析、填充符号表的过程中重新处理这些新符号。javac编译过程如下:

图1-1 Javac 编译过程

注解处理器

JDK 5之后,Java语言提供了对注解(Annotations)的支持,注解在设计上原本是与普通的Java代码一样,都只会在程序运行期间发挥作用的。
但在JDK 6中又提出并通过了JSR-269提案,该提案设计了一组被称为“插入式注解处理器”的标准API,可以提前至编译期对代码中的特定注解进行处理
从而影响到前端编译器的工作过程。可以把插入式注解处理器看作是一组编译器的插件,当这些插件工作时,允许读取、修改、添加抽象语法树中的任意元素
如果这些插件在处理注解期间对语法树进行过修改,编译器将回到解析及填充符号表的过程重新处理,直到所有插入式注解处理器都没有再对语法树进行修改为止。
典型示例为Java著名的编码效率工具Lombok,它可以通过注解来实现自动产生getter/setter方法、进行空置检查、生成受查异常表、产生equals()和hashCode()方法等等。

要通过注解处理器API实现一个编译器插件,首先需要了解这组API的一些基本知识。实现注解处理器的代码需要继承抽象类javax.annotation.processing.AbstractProcessor,
这个抽象类中只有一个子类必须实现的抽象方法:process(),它是Javac编译器在执行注解处理器代码时要调用的过程,从这个方法的第一个参数“annotations”中获取到
此注解处理器所要处理的注解集合,从第二个参数“roundEnv”中访问到当前这个轮次(Round)中的抽象语法树节点,每个语法树节点在这里都表示为一个Element。

除了process()方法的传入参数之外,还有一个很重要的实例变量processingEnv,它是AbstractProcessor中的一个protected变量,在注解处理器初始化的时候(init()方法执行的时候)创建,
继承了AbstractProcessor的注解处理器代码可以直接访问它。它代表了注解处理器框架提供的一个上下文环境,要创建新的代码、向编译器输出信息、获取其他工具类等都需要用到这个实例变量。

public abstract class AbstractProcessor implements Processor {
    /**
     * Processing environment providing by the tool framework.
     */
    protected ProcessingEnvironment processingEnv;
    private boolean initialized = false;

    public abstract boolean process(Set<? extends TypeElement> annotations,
                                    RoundEnvironment roundEnv);

}

语法糖

几乎所有的编程语言都或多或少提供过一些语法糖来方便程序员的代码开发,这些语法糖虽然不会提供实质性的功能改进,
但是它们或能提高效率,或能提升语法的严谨性,或能减少编码出错的机会。现在也有一种观点认为语法糖并不一定都是有益的,
大量添加和使用含糖的语法,容易让程序员产生依赖,无法看清语法糖的糖衣背后,程序代码的真实面目。

泛型
泛型的本质是参数化类型(Parameterized Type)或者参数化多态(Parametric Polymorphism)的应用,即可以将操作的数据类型指定为方法签名中的一种特殊参数,
这种参数类型能够用在类、接口和方法的创建中,分别构成泛型类、泛型接口和泛型方法。泛型让程序员能够针对泛化的数据类型编写相同的算法,增强了编程语言的类型系统及抽象能力。
Java选择的泛型实现方式叫作“类型擦除式泛型”(Type Erasure Generics),List< Integer > 与 List< String >仅在程序源码中是不同的,编译之后泛型都被替换成了裸类型
并在相应地方插入强制转型代码。因此在运行期,List < Integer >与List< String >是同一个类型。裸类型实现是简单粗暴地直接在编译时把ArrayList< Integer >还原回ArrayList,
只在元素访问、修改时自动插入一些强制类型转换和检查指令。如下例所示:

public static void main(String[] args) {
 Map<String, String> map = new HashMap<String, String>();
 map.put("hello", "Hai");
 map.put("how are you?", "I\'m fine.");
 System.out.println(map.get("hello"));
 System.out.println(map.get("how are you?"));
}

// 泛型擦除后
public static void main(String[] args) {
 Map map = new HashMap();
 map.put("hello", "Hai");
 map.put("how are you?", "I\'m fine.?");
 System.out.println((String) map.get("hello"));
 System.out.println((String) map.get("how are you?"));
}

自动拆装箱、循环遍历

    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4);
        int sum = 0;
        for (int i : list) {
            sum += i;
        }
        System.out.println(sum);
    }
    // 编译之后
    public static void main(String[] args) {
        List list = Arrays.asList( new Integer[] {
            Integer.valueOf(1),
            Integer.valueOf(2),
            Integer.valueOf(3),
            Integer.valueOf(4) });
        int sum = 0;
        for (Iterator localIterator = list.iterator(); localIterator.hasNext(); ) {
            int i = ((Integer)localIterator.next()).intValue();
            sum += i;
        }
        System.out.println(sum);
    }

以上是关于Javac 编译器的主要内容,如果未能解决你的问题,请参考以下文章

66.javac 编译与 JIT 编译编译过程javac 编译词法语法分析填充符号表语义分析字节码生成JIT 编译

javac编译原理

Javac编译原理

Javac编译与JIT编译

Javac的实现过程

Javac早期(编译期)