[TOC]
10.2 Javac 編譯器
10.2.1 編譯過(guò)程
- 從Sun Javac 的代碼來(lái)看,編譯過(guò)程大致可以分為3個(gè)過(guò)程,分別是:
- 解析與填充符號(hào)表過(guò)程。
- 插入式注解處理器的注解處理過(guò)程。
- 分析與字節(jié)碼生成過(guò)程。

編譯過(guò)程
10.2.2 解析與填充符號(hào)表
- 解析步驟包括了經(jīng)典程序編譯原理中的詞法分析和語(yǔ)法分析兩個(gè)過(guò)程。
10.2.2.1 詞法、語(yǔ)法分析
- 詞法分析是將源代碼的字符流轉(zhuǎn)變?yōu)闃?biāo)記(Token)集合,單個(gè)字符是程序編寫(xiě)過(guò)程的最小元素,而標(biāo)記則是編譯過(guò)程的最小元素,關(guān)鍵字、變量名、字面量、運(yùn)算符都可以成為標(biāo)記
- 語(yǔ)法分析是根據(jù) Token 序列構(gòu)造抽象語(yǔ)法樹(shù)的過(guò)程,抽象語(yǔ)法樹(shù)(Abstract Syntax Tree,AST)是一種用來(lái)描述程序代碼語(yǔ)法結(jié)構(gòu)的樹(shù)形表示方式,語(yǔ)法樹(shù)的每一個(gè)節(jié)點(diǎn)都代表著程序代碼中的一個(gè)語(yǔ)法結(jié)構(gòu)(Construct),例如包、類(lèi)型、修飾符、運(yùn)算符、接口、返回值甚至代碼注釋等都可以是一個(gè)語(yǔ)法結(jié)構(gòu)。
- 經(jīng)過(guò)語(yǔ)法分析之后,編譯器就基本不會(huì)再對(duì)源碼文件進(jìn)行操作了,后續(xù)的操作都建立在抽象語(yǔ)法樹(shù)之上。
10.2.2.2 填充符號(hào)表
- 完成了語(yǔ)法分析和詞法分析之后,下一步就是填充符號(hào)表的過(guò)程。
- 符號(hào)表(Symbol Table)是由一組符號(hào)地址和符號(hào)信息構(gòu)成的表格,可以把它想象成哈希表中 K-V 值對(duì)的形式(實(shí)際上符號(hào)表不一定是哈希表實(shí)現(xiàn),可以是有序符號(hào)表、樹(shù)狀符號(hào)表、棧結(jié)構(gòu)符號(hào)表等)。符號(hào)表中所登記的信息在編譯的不同階段都要用到。
- 在語(yǔ)義分析中,符號(hào)表所登記的內(nèi)容將用于語(yǔ)義檢查(如檢查一個(gè)名字的使用和原先的說(shuō)明是否一致)和產(chǎn)生中間代碼。在目標(biāo)代碼生成階段,當(dāng)對(duì)符號(hào)名進(jìn)行地址分配時(shí),符號(hào)表是地址分配的依據(jù)。
10.2.3 注解處理器
- 在 JDK1.5 之后,Java 語(yǔ)言提供了對(duì)注解(Annotation)的支持,這些注解與普通的 Java 代碼一樣,是在運(yùn)行期間發(fā)揮作用的。
- 在 JDK1.6 中提供了一組插入式注解處理器的標(biāo)準(zhǔn)API,在編譯期間對(duì)注解進(jìn)行處理,我們可以把它看做是一組編譯器的插件。在這些插件里面,可以讀取、修改、添加抽象語(yǔ)法樹(shù)中的任意元素
- 如果這些插件在處理注解期間對(duì)語(yǔ)法樹(shù)進(jìn)行了修改,編譯器將回到解析及填充符號(hào)表的過(guò)程重新處理,直到所有插入式注解處理器都沒(méi)有再對(duì)語(yǔ)法樹(shù)進(jìn)行修改為止,每一次循環(huán)稱(chēng)為一個(gè)Round。
10.2.4 語(yǔ)義分析與字節(jié)碼生成
- 語(yǔ)法分析之后,編譯器獲得了程序代碼的抽象語(yǔ)法樹(shù)表示,語(yǔ)法樹(shù)能表示一個(gè)結(jié)構(gòu)正確的源程序的抽象,但無(wú)法保證源程序是符合邏輯的。而語(yǔ)義分析的主要任務(wù)是對(duì)結(jié)構(gòu)上正確的源程序進(jìn)行上下文有關(guān)性質(zhì)的審查。Javac 的編譯過(guò)程中,語(yǔ)義分析過(guò)程分為標(biāo)注檢查以及數(shù)據(jù)及控制流分析兩個(gè)步驟
10.2.4.1 標(biāo)注檢查
- 標(biāo)注檢查步驟檢查的內(nèi)容包括諸如變量使用前是否已被聲明、變量與賦值之間的數(shù)據(jù)類(lèi)型是否能夠匹配等。
- 在標(biāo)注檢查步驟中,還有一個(gè)重要的動(dòng)作稱(chēng)為常量折疊
10.2.4.2 數(shù)據(jù)及控制流分析
- 數(shù)據(jù)及控制流分析是對(duì)程序上下文邏輯更進(jìn)一步的驗(yàn)證,它可以檢查出諸如程序局部變量在使用前是否有賦值、方法的每條路徑是否都有返回值、是否所有的受查異常都被正確處理了等問(wèn)題。
- 編譯時(shí)期的數(shù)據(jù)及控制流分析與類(lèi)加載時(shí)的數(shù)據(jù)及控制流分析的目的基本上是一致的,但校驗(yàn)范圍有所區(qū)別,有一些校驗(yàn)項(xiàng)只有在編譯期或運(yùn)行期才能進(jìn)行。
10.2.4.3 解語(yǔ)法糖
- 語(yǔ)法糖(Syntactic Sugar),也稱(chēng)糖衣語(yǔ)法,指在計(jì)算機(jī)語(yǔ)言中添加的某種語(yǔ)法,這種語(yǔ)法對(duì)語(yǔ)言的功能并沒(méi)有影響,但是更方便程序員使用。通常來(lái)說(shuō),使用語(yǔ)法糖能夠增加程序的可讀性,從而減少程序代碼出錯(cuò)的機(jī)會(huì)。
- Java 中最常用的語(yǔ)法糖主要是前面提到過(guò)的泛型、變長(zhǎng)參數(shù)、自動(dòng)裝箱/拆箱等
- 但虛擬機(jī)運(yùn)行時(shí)不支持這些語(yǔ)法,它們在編譯階段還原回簡(jiǎn)單的基礎(chǔ)語(yǔ)法結(jié)構(gòu),這個(gè)過(guò)程稱(chēng)為解語(yǔ)法糖。
10.2.4.4 字節(jié)碼生成
字節(jié)碼生成是 Javac 編譯過(guò)程的最后一個(gè)階段,字節(jié)碼生成階段不僅僅是把前面各個(gè)步驟所生成的信息(語(yǔ)法樹(shù)、符號(hào)表)轉(zhuǎn)化成字節(jié)碼寫(xiě)到磁盤(pán)中,編譯器還進(jìn)行了少量的代碼添加和轉(zhuǎn)換工作。
實(shí)例構(gòu)造器
<init>()方法和類(lèi)構(gòu)造器<clinit>()方法就是在這個(gè)階段添加到語(yǔ)法樹(shù)之中的。-
這兩個(gè)構(gòu)造器的產(chǎn)生過(guò)程實(shí)際上是一個(gè)代碼收斂的過(guò)程,編譯器會(huì)把如下操作收斂到
<init>()和<clinit>()方法之中,并且保證一定是按先執(zhí)行父類(lèi)的實(shí)例構(gòu)造器,然后初始化變量,最后執(zhí)行語(yǔ)句塊的順序進(jìn)行:- 語(yǔ)句塊(對(duì)于實(shí)例構(gòu)造器而言是 “{}” 塊,對(duì)于類(lèi)構(gòu)造器而言是 “static{}” 塊)
- 變量初始化(實(shí)例變量和類(lèi)變量)
- 調(diào)用父類(lèi)的實(shí)例構(gòu)造器等操作
完成了對(duì)語(yǔ)法樹(shù)的遍歷和調(diào)整之后,再把填充了所有所需信息的符號(hào)表轉(zhuǎn)成字節(jié)碼,生成最終的 Class 文件,到此為止整個(gè)編譯過(guò)程宣告結(jié)束。
10.3 Java語(yǔ)法糖的味道
10.3.1 泛型與類(lèi)型擦除
- 泛型是 JDK1.5 的一項(xiàng)新增特性,它的本質(zhì)是參數(shù)化類(lèi)型(Parametersized Type)的應(yīng)用,也就是說(shuō)所操作的數(shù)據(jù)類(lèi)型被指定為一個(gè)參數(shù)。這種參數(shù)類(lèi)型可以用在類(lèi)、接口和方法的創(chuàng)建中,分別稱(chēng)為泛型類(lèi)、泛型接口和泛型方法。
- Java 語(yǔ)言中的泛型只在程序源碼中存在,在編譯后的字節(jié)碼文件中,就已經(jīng)替換為原來(lái)的原生類(lèi)型(Raw Type,也稱(chēng)為裸類(lèi)型)了,并且在相應(yīng)的地方插入了強(qiáng)制轉(zhuǎn)型代碼,因此,對(duì)于運(yùn)行期的 Java 語(yǔ)言來(lái)說(shuō),
ArrayList<int>與ArrayList<String>就是同一個(gè)類(lèi) - 所以泛型技術(shù)實(shí)際上是 Java 語(yǔ)言的一顆語(yǔ)法糖,Java語(yǔ)言中的泛型實(shí)現(xiàn)方法稱(chēng)為類(lèi)型擦除,基于這種方法實(shí)現(xiàn)的泛型稱(chēng)為偽泛型。
public class GenericTypes
{
// 'method(List<String>)' clashes with 'method(List<Integer>)'; both methods have same erasure
// 編譯錯(cuò)誤,因?yàn)轭?lèi)型擦除后參數(shù)類(lèi)型相同,都是 List<E>
public static void method(List<String> list)
{
System.out.println("invoke method(List<String> list)");
}
public static void method(List<Integer> list)
{
System.out.println("invoke method(List<Integer> list)");
}
}
- 上述這段代碼是不能被編譯的,因?yàn)閰?shù)
List<Integer>和List<String>編譯之后都被擦除了,變成了一樣的原生類(lèi)型List<E>,擦除動(dòng)作導(dǎo)致這兩種方法的特征簽名變得一模一樣。
10.3.2 自動(dòng)裝箱、拆箱與遍歷循環(huán)
從純技術(shù)的角度來(lái)講,自動(dòng)裝箱、自動(dòng)拆箱與遍歷循環(huán)(Foreach循環(huán))這些語(yǔ)法糖,無(wú)論是實(shí)現(xiàn)上還是思想上都不能和泛型相比,兩者的難度和深度都有很大差距。但是毫無(wú)疑問(wèn),它們是 Java 語(yǔ)言里使用得最多的語(yǔ)法糖。
自動(dòng)裝箱、拆箱與遍歷循環(huán)
// 編譯前
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);
}
- 上述代碼包含了泛型、自動(dòng)裝箱、自動(dòng)拆箱、遍歷循環(huán)與變長(zhǎng)參數(shù) 5 種語(yǔ)法糖,以及它們?cè)诰幾g后的變化:
- 泛型擦除已經(jīng)說(shuō)過(guò)。
- 自動(dòng)裝箱、拆箱在編譯之后被轉(zhuǎn)化成了對(duì)應(yīng)的包裝和還原方法
- 遍歷循環(huán)把代碼還原成了迭代器的實(shí)現(xiàn),這也是為何遍歷循環(huán)需要被遍歷的類(lèi)實(shí)現(xiàn)Iterable接口的原因。
- 變長(zhǎng)參數(shù)在調(diào)用的時(shí)候變成了一個(gè)數(shù)組類(lèi)型的參數(shù),在變長(zhǎng)參數(shù)出現(xiàn)之前,程序員就是使用數(shù)組來(lái)完成類(lèi)似功能的。