JVM之javac编译器、java语法糖

1.概述

java语言编译期可能有三种情况:

  • 把.java文件转变成.class文件的过程,前期的编译器,像IDE中的编译器。
  • 把字节码转变为机器码的过程,像运行期编译器JIT编译器(Just In Time Compiler)。
  • 直接把.java文件转变为本地及其代码的过程,静态提前编译器AOT编译器(Ahead Of Time Compiler)。

2.javac编译器

javac的编译过程大致分为三个过程,分别是:

  • 解析与填充符号表。
  • 插入式注解处理器的注解处理过程。
  • 分析与字节码生成过程。
    这三个步骤的交互关系与交互顺序如图:
插入式注解处理器修改语法树
入口
解析与填充符号表
注解处理
分析与字节码生成
010101

2.1.解析与填充符号表

2.1.1 解析步骤

解析步骤包含了编译原理中的词法分析与语法分析两个过程。

  • 词法分析是将源代码的字符流转变为标记(Token)集合,单个字符是程序编写过程中的最小元素,而标记是编译过程中的最小元素,关键字、变量名、字面量和运算符都可以成为标记。
  • 语法分析是根据Token序列来构造抽象语法树的过程,抽象语法树是一种用来描述程序代码语法结构的树形标识方式,语法树的每个节点都代表着程序代码中的一个语法结构,例如包、类型、修饰符、运算符、接口、返回值甚至代码注释等可以是一个语法结构。经过语法分析之后,编译器就基本不会再对源码文件进行操作了,后续的操作都建立在抽象语法树之上。

2.1.2 填充符号表

完成词法分析和语法分析之后,下一步就是填充符号表的过程。符号表是由一组符号地址和符号信息构成的表格,可以把它想象成哈希表中的K-V值对的形式(实际上符号表不一定是哈希表实现,可以是有序符号表、树状符号表和栈结构符号表等)。符号表所登记的信息在编译的不同阶段都要用到。在语义分析中,符号表所登记的内容将用于语义检查(如检查一个名字的使用和原先的说明是否一致)和产生中间代码。在目标代码生成阶段,当对符号名进行地址分配时,符号表是地址分配的依据。

2.2.注解处理器

JDK1.5之后提供了对注解(Annotations)的支持,这些注解与普通的java代码一样,是在运行期间发挥作用的。
JDK1.6中又提供了一组插入式注解处理器的标准API在编译期间对注解进行处理,我们可以把它看做是编译器的一组插件,在这些插件里面,可以读取、修改、添加抽象语法树中的任意元素,如果这些插件在处理注解期间对语法树进行了修改,那么编译器将回到解析及填充符号表的过程重新处理,直到所有的插入式注解处理器都没有再对语法树进行修改为止,每一次回环称为一个Round,即上图的的回环过程。
有了编译器注解处理的标准API后,我们的代码才有可能干涉编译器的行为,由于语法树的任何元素,设置代码注释都可以在插件之后访问到,所以通过插入式注解处理器实现的插件在功能上有很大的发挥空间。

2.3.语义分析与字节码生成

语义分析的主要任务是对结构上正确的源程序进行上下文有关性质的审查,如进行类型检查。语义分析分为标注检查数据及控制流分析两个步骤,生成字节码之前还会有解语法糖步骤。

2.3.1.标注检查

检查的内容包括诸如变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配等。此步骤中,还有一个重要的动作称为常量折叠
如果在代码中定义了 int a = 1 + 2;,在语法树上仍然能够看到字面量“1”,“2”,“+”,但是经过常量折叠之后,他们将被折叠成“3”,这个插入式表达式的值已经在语法树上标注出来了。由于常量折叠的存在,在编译期直接定义int a = 1 + 2;与定义int a = 3;相比不会增加程序运行期的运算量

2.3.2.数据及控制流分析

数据及控制流分析是对程序上下文逻辑的更进一步验证,它可以检查出程序局部变量在使用前是否有赋值、方法的每条路径是否都有返回值、是否所有的受查异常都被正确处理了等问题。编译时期的数据及控制流分析与类加载时期的数据及控制流分析目的基本一样,但是校验范围有些区别,有些校验只能在编译期或者运行期才能进行。
代码示例:

//带有final修饰
public void foo(final int art){
	final int var = 0;
	//do something
}
//没有final修饰
public void foo(int art){
	int var = 0;
	//do something
}

以上代码在IED中是无法通过编译的,因为两段代码编译出来的class是没有任何区别的,而使用final时,程序只会在代码编写阶段受到final的影响,对运行期是没有任何影响的,变量的不变性仅仅由编译器在编译期间保障。

2.3.3.字节码生成

字节码生成是javac编译过程的最后一个阶段,字节码生成阶段不仅仅是把前面各个步骤所生成的信息(语法树、符号表)转化成字节码写到磁盘中,编译器还进行了少量的代码添加和替换工作。

3.语法糖

语法糖是指在计算机语言中添加的某种语法,这种语法对语言的功能没有影响,到哪是更方便程序员使用,通常使用语法糖能增加程序的可读性。编译期在生成字节码之前会将这些语法结构还原成基础语法结构,这个步骤称为解语法糖。
java中的语法糖主要有泛型、自动拆装箱、遍历循环、变长参数、条件编译、内部类、枚举类、断言、switch 支持 String 与枚举(JAVA7)、try-with-resource(JAVA7)、数字字面量(JAVA7)、Lambda(JAVA8)。

3.1.泛型与类型擦除

泛型是JDK1.5的心特性,本质是参数化类型(Parameterized Type)的应用,它可以用在类、接口、方法的创建中,分别称为泛型类、泛型接口、泛型方法。在java中泛型只存在与程序源码中,编译后的字节码文件中就已经被替换为原来的原生类型(Raw Type)了,并且在相应的地方插入了强制转型代码,如对于运行期来说,ArrayList<int>与ArrayList<String>都是ArrayList。所以对于java来说泛型就是语法糖,此种实现方式称为类型擦除。

3.2.自动装箱、拆箱与遍历循环

自动拆装箱和遍历循环是java中使用的最多的语法糖,其中有几个重要的小知识点:

  • “==”在遇到算术运算符会自动进行拆箱,没有遇到算术运算符不会自动拆箱。
  • equals()方法不会处理数据类型转换的关系。
  • 在java5中,在Integer的操作上为了节省内存和提高性能,整型对象通过使用相同的对象引用来实现缓存和重用(适用的整数区间-128~127)。
  • 集合的增强for循环会被还原成迭代器遍历。
    如下代码:
List<Integer> list = Arrays.asList(1,2,3,4);
int sum = 0;
for (int i : list) {
    sum+=i;
}

编译后代码为:

List<Integer> list = Arrays.asList(new Integer[] { 
	null, 
	null, 
	null, 
	(new Integer[4][2] = (new Integer[4][1] = (new Integer[4][0] = Integer.valueOf(1)).valueOf(2)).valueOf(3)).valueOf(4) 
});
    
int sum = 0;
for (Iterator i$ = list.iterator(); i$.hasNext(); ) { 
    int i = ((Integer)i$.next()).intValue();
    sum += i; 
}

测试代码:

    public static void main(String[] args) {
        Integer a =1;
        Integer b =2;
        Integer c =3;
        Integer d =3;
        Integer e =127;
        Integer f =127;
        Long g =3L;

        System.out.println(c==d);
        System.out.println(e==f);
        System.out.println(c==(a+b));
        System.out.println(c.equals(a+b));
        System.out.println(g==(a+b));//“==”在遇到算术运算符会自动进行拆箱,没有遇到算术运算符不会自动拆箱
        System.out.println(g.equals(a+b));//equals()方法不会处理数据类型转换的关系
    }

输出如下:

true
true
true
true
true
false

3.3.条件编译

java中的条件编译是指在使用条件为常量的if语句,在编译阶段会将条件中不满足条件的分支给消除掉,如下:

if(true){
            System.out.println(111);
        }else{
            System.out.println(222);
        }

经过编译后,代码如下:

System.out.println(111);

3.4.变长参数

变长参数其实就是参数使用数组,使用的时候先创建一个数组,数组的长度就是参数的个数,最终将数据传入到被调用的方法中。
测试代码如下:

public class test2 {
    public static void main(String[] args) {
        test("sss","ddd","eee");
    }

    public static void test(String... strs)
    {
        for (int i = 0; i < strs.length; i++)
        {
            System.out.println(strs[i]);
        }
    }
}

反编译后代码如下:
在这里插入图片描述

3.5.内部类、枚举类

拥有内部类的文件outer.class在编译后会生成一个名为outer$inner.class的内部类文件。
枚举类编译后会生成一个final类型的class。
枚举测试类如下:

public enum testEnum {
    SPRING,SUMMER;
}

使用jad反编译后代码如下:

public final class testEnum extends Enum
{

    public static testEnum[] values()
    {
        return (testEnum[])$VALUES.clone();
    }

    public static testEnum valueOf(String name)
    {
        return (testEnum)Enum.valueOf(com/glt/compile/testEnum, name);
    }

    private testEnum(String s, int i)
    {
        super(s, i);
    }

    public static final testEnum SPRING;
    public static final testEnum SUMMER;
    private static final testEnum $VALUES[];

    static 
    {
        SPRING = new testEnum("SPRING", 0);
        SUMMER = new testEnum("SUMMER", 1);
        $VALUES = (new testEnum[] {
            SPRING, SUMMER
        });
    }
}

3.6.断言

java中断言默认不开启,开启需要使用JVM添加参数-ea,断言的底层实现是使用if语句,如果断言结果为true就继续执行,如果断言失败抛出AssertionError异常。
测试代码如下:

public class testAssert {

    public static void main(String args[]) {
        int a = 1;
        int b = 1;
        assert a == b;
        System.out.println("true");
        assert a != b : "aaa";
        System.out.println("false");
    }
}

使用jad反编译后:

public class testAssert
{

    public testAssert()
    {
    }

    public static void main(String args[])
    {
        int a = 1;
        int b = 1;
        if(!$assertionsDisabled && a != b)
            throw new AssertionError();
        System.out.println("true");
        if(!$assertionsDisabled && a == b)
        {
            throw new AssertionError("aaa");
        } else
        {
            System.out.println("false");
            return;
        }
    }

    static final boolean $assertionsDisabled = !com/glt/compile/testAssert.desiredAssertionStatus();

}

3.7.switch 支持 String 与枚举(JAVA7)

java编译器中switch支持byte、short、char、int、String,最终的比较都是转换成整型,代码编译后就会将各种类型转换为整型。
测试代码如下:

public class TestSwitch {
    public static void main(String[] args) {
        String str = "a";
        switch (str){
            case "a":
                System.out.println("aaaa");
                break;
            case "b":
                System.out.println("bbbb");
                break;
            default:
                break;
        }
    }
}

使用jad反编译后;

public class TestSwitch
{

    public TestSwitch()
    {
    }

    public static void main(String args[])
    {
        String str = "a";
        String s = str;
        byte byte0 = -1;
        switch(s.hashCode())
        {
        case 97: // 'a'
            if(s.equals("a"))
                byte0 = 0;
            break;

        case 98: // 'b'
            if(s.equals("b"))
                byte0 = 1;
            break;
        }
        switch(byte0)
        {
        case 0: // '\0'
            System.out.println("aaaa");
            break;

        case 1: // '\001'
            System.out.println("bbbb");
            break;
        }
    }
}

3.8.try-with-resource(JAVA7)

通常我们读文件或者写连接池等都需要手动在finally里面关闭流或者连接,但是java7的try-with-resource为我们关闭连接或者流,不用我们自己再调用关闭连接或者关闭流。
测试代码如下:

public class TestTryWithResource {

    public static void main(String[] args) {
        try (BufferedReader br = new BufferedReader(new FileReader("input.txt"))) {
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

反编译后代码如下:

public class TestTryWithResource
{

    public TestTryWithResource()
    {
    }

    public static void main(String args[])
    {
        BufferedReader br;
        Throwable throwable;
        br = new BufferedReader(new FileReader("input.txt"));
        throwable = null;
        String line;
        try
        {
            while((line = br.readLine()) != null) 
                System.out.println(line);
        }
        catch(Throwable throwable1)
        {
            throwable = throwable1;
            throw throwable1;
        }
        if(br != null)
            if(throwable != null)
                try
                {
                    br.close();
                }
                catch(Throwable x2)
                {
                    throwable.addSuppressed(x2);
                }
            else
                br.close();
        break MISSING_BLOCK_LABEL_117;
        Exception exception;
        exception;
        if(br != null)
            if(throwable != null)
                try
                {
                    br.close();
                }
                catch(Throwable x2)
                {
                    throwable.addSuppressed(x2);
                }
            else
                br.close();
        throw exception;
        IOException e;
        e;
        e.printStackTrace();
    }
}

3.9.数字字面量(JAVA7)

在java 7中新增的数字字面量定义:不管是整数还是浮点数,都允许在数字之间插入任意多个下划线。这些下划线不会对字面量的数值产生影响,目的就是方便阅读。

int v = 10_00_00;

反编译后:

int v = 100000;

3.10.Lambda(JAVA8)

Lambda表达式为JAVA8新增的新特性,lambda表达式的实现其实是依赖了一些底层的api,在编译阶段,编译器会把lambda表达式进行解糖,转换成调用内部api的方式。

4.总结

本文介绍了编译器编译文件为字节码文件过程,了解了编译期间存在的语法糖,插入式注解处理器等手段,主要是为了能够提升程序的编码效率,现在好多的IDE的插件像Lombok等都是为了提升编码效率。

发布了61 篇原创文章 · 获赞 85 · 访问量 17万+

猜你喜欢

转载自blog.csdn.net/bluuusea/article/details/94359937