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等都是爲了提升編碼效率。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章