本文涉及的javac編譯器來自openjdk.
javac的目錄地址為:
解壓目錄/langtools/src/share/classes/com/sun/tools/javac/
javac編譯器將Java編譯成為一個(gè)有效的字節(jié)碼文件會(huì)經(jīng)歷4個(gè)步驟:
- 詞法解析:將Java關(guān)鍵字排序,使得程序能有序運(yùn)行叮盘。
- 語法解析:詞法解析后的Token序列整合為一顆抽象的語法樹糯彬。
- 語義解析:將抽象語法樹擴(kuò)展地更加完善溺森。
- 字節(jié)碼解析:將字節(jié)碼解析成完整的類。
詞法解析
詞法解析是編譯器執(zhí)行的字節(jié)碼編譯的第一步邢隧。這個(gè)步驟中,將Java源碼中關(guān)鍵字和標(biāo)識符等轉(zhuǎn)換成符合規(guī)范的Token序列。
詞法解析器的接口是com.sun.tools.javac.parser.Lexer ,它直接派生于同包下面的Scanner類陨舱,它的主要任務(wù)是按照單個(gè)字符的方式讀取Java源文件中的關(guān)鍵字和標(biāo)識符等,然后將其轉(zhuǎn)換為符合Java規(guī)范的Token序列版仔。而負(fù)責(zé)詞法解析工作的是com.sun.tools.javac.parser.JavacParser類游盲,該類的對象實(shí)例由ParseFactory負(fù)責(zé)創(chuàng)建,JavacParser負(fù)責(zé)詞法解析的具體細(xì)節(jié)蛮粮。
當(dāng)我們在命令行敲入javac的時(shí)候背桐,Java首先會(huì)調(diào)用com.sun.tools.javac.main.Main類的compile()方法。compile()方法接著就會(huì)調(diào)用JavaCompiler類的parseFile()方法蝉揍,parseFile()的主要功能就是調(diào)用自己的parse()方法獲得JavacParser實(shí)例對象链峭,然后調(diào)用JavacParser類的parseCompilationUnit()進(jìn)行詞法解析。
這個(gè)過程如下圖所示:
Token序列
Token其實(shí)就是一個(gè)枚舉類型又沾,其內(nèi)部定了許多符合Java語法規(guī)范并與源碼字符集相對應(yīng)的枚舉常量弊仪。
所有的枚舉常量都在 com.sun.tools.javac.parser.Token類中。
編譯器在執(zhí)行詞法解析的過程中杖刷,只會(huì)對Token進(jìn)行匹配校驗(yàn)励饵。
源碼字符集是如何轉(zhuǎn)換成Token的:
Name對象和Token對象建立的是一種一對一的關(guān)系。當(dāng)詞法解析器中需要將一個(gè)源碼字符集合解析成一個(gè)Token時(shí)滑燃,它會(huì)通過Names類調(diào)用Name類的fromChars()方法獲得一個(gè)Name對象役听,然后使用Keyswords類的key(Name name)方法獲得傳入相對應(yīng)的Token對象。
詞法解析器如何保存源碼字符集和Token之間的對應(yīng)關(guān)系:
詞法解析器在將源碼轉(zhuǎn)字符集合轉(zhuǎn)換為Token之前,會(huì)先將每一個(gè)字符集合都轉(zhuǎn)換成一個(gè)對應(yīng)的Name對象典予。接著再由com.sun.tools.javac.parser.Keywords類負(fù)責(zé)實(shí)際的Token轉(zhuǎn)換任務(wù)(將Token常量全部轉(zhuǎn)換為Name對象)甜滨,然后轉(zhuǎn)換好的這些Name對象全部存到Name類的內(nèi)部類Table中,Keywords類中的數(shù)組key用于保存源碼字符集合和Token之間的對應(yīng)關(guān)系瘤袖。
以上兩個(gè)問題略微有些復(fù)雜衣摩。畫了一個(gè)圖來表示一下:
調(diào)用nextToken()計(jì)算Token的獲取順序
Keywords類的key()方法僅僅是根據(jù)Name對象獲得對應(yīng)的Token,而詞法解析器是通過Scanner類的nextToken()方法保證Token的讀取順序規(guī)則捂敌。
調(diào)用parseCompilationUnit()方法執(zhí)行詞法解析
詞法解析的核心是校驗(yàn)Token是否匹配com.sun.tools.javac.parser.JavacParser類在parseCompilationUnit()方法定義的匹配規(guī)則艾扮。parseCompilationUnit()方法會(huì)按照Token的匹配順序依次解析出package、import等關(guān)鍵字占婉,當(dāng)這些Token匹配之后泡嘴,詞法解析器會(huì)開始解析class主題信息,直到詞法解析全部結(jié)束逆济,parseCompilationUnit()方法會(huì)將Token轉(zhuǎn)換為一棵結(jié)構(gòu)化的抽象語法樹酌予。
語法解析
之前提過,語法解析的目的就是將經(jīng)過詞法解析得到的Token整合為一棵結(jié)構(gòu)化的抽象語法樹纹腌。
詞法解析完成的Token序列依舊還不完善霎终,它們還沒有被整合起來,語法解析的主要任務(wù)是把這些零散的Token按照指定的Java語法規(guī)范整合起來形成一個(gè)有機(jī)的整體升薯。
在語法解析階段莱褒,語法樹上每個(gè)節(jié)點(diǎn)都直接或者間接地繼承了JCTree類。
調(diào)用qualident()方法解析package語法節(jié)點(diǎn)
parseCompilationUnit()這個(gè)方法實(shí)際上涎劈,跨越了詞法解析和語法解析兩個(gè)階段广凸。當(dāng)詞法解析器成功將package關(guān)鍵字聲明轉(zhuǎn)換為Token并完成詞法解析之后,會(huì)調(diào)用qualident()方法根據(jù)Token.PACKAGE解析為package語法節(jié)點(diǎn)蛛枚。
語法解析步驟中谅海,com.sun.tools.javac.tree.TreeMaker負(fù)責(zé)創(chuàng)建JCTree類的所有語法節(jié)點(diǎn)對象實(shí)例。所以TreeMaker本身就是一個(gè)語法解析器蹦浦,不過具體的細(xì)節(jié)由parseCompilationUnit()方法來控制扭吁。
語法解析器實(shí)質(zhì)上還是使用Token對應(yīng)的Name對象,來作為轉(zhuǎn)換語法節(jié)點(diǎn)的素材盲镶。所以在解析語法樹之前侥袜,首先需要將Token轉(zhuǎn)換成對應(yīng)的Name對象。語法解析器就可以根據(jù)Name對象解析出一個(gè)JCIdent語法節(jié)點(diǎn)溉贿。
當(dāng)一個(gè)package關(guān)鍵字聲明中定義了多級目錄時(shí)枫吧,qualident()方法就會(huì)循環(huán)迭代調(diào)用語法解析器將package關(guān)鍵字聲明解析為嵌套的JCFieldAccess語法節(jié)點(diǎn)。
調(diào)用importDeclaration()方法解析import語法樹
當(dāng)成功解析出package語法節(jié)點(diǎn)之后宇色,parseCompilationUnit()這個(gè)節(jié)點(diǎn)會(huì)調(diào)用importDeclaration()方法來解析得到import語法樹九杂。
importDeclaration()首先匹配Token.Static來檢測import語句中是否包含了靜態(tài)導(dǎo)入颁湖。接著importDeclaration()就會(huì)調(diào)用語法解析器的Ident()方法解析出一個(gè)JCIdent語法節(jié)點(diǎn),如果import語句中包含多級目錄的時(shí)候例隆,語法解析器就會(huì)調(diào)用Select()方法解析為嵌套的JCFieldAccess語法節(jié)點(diǎn)甥捺。
當(dāng)語法解析器成功解析出JCIdent和JCFieldAccess節(jié)點(diǎn)之后,importDeclaration()方法會(huì)調(diào)用import()方法裳擎,將之前解析的語法節(jié)點(diǎn)涎永,整合成為一棵JCImport節(jié)點(diǎn)思币。
實(shí)際開發(fā)中鹿响,通常會(huì)有多個(gè)import關(guān)鍵字聲明,那么importDeclaration()方法內(nèi)部會(huì)通過迭代循環(huán)方法解析出多個(gè)JCImport語法樹谷饿,然后將其存儲在一個(gè)集合中惶我。
調(diào)用classDeclaration()方法解析class語法樹
語法解析的最后一步就是解析class的主體信息,當(dāng)語法解析器成功將import關(guān)鍵字解析聲明為JCIdent和JCFieldAccess語法節(jié)點(diǎn)并整合為一顆JCImport語法樹之后博投,parseCompilationUnit()方法內(nèi)部通過typeDeclaration()方法調(diào)用classOrInterfaceOrEnumDeclaration方法將class主體信息解析為一棵JCClassDecl語法樹绸贡。
當(dāng)以上這個(gè)過程結(jié)束之后,parseCompilationUnit()方法會(huì)調(diào)用TopLevel()方法將之前解析好過的package語法節(jié)點(diǎn)毅哗,import語法節(jié)點(diǎn)和class語法樹等內(nèi)容內(nèi)容信息全部整合成一棵JCCompilationUnit語法節(jié)點(diǎn)樹听怕。
JCCompilationUnit類會(huì)成為整個(gè)語法樹的Root,持有整個(gè)語法樹的所有節(jié)點(diǎn)虑绵。這個(gè)時(shí)候尿瞭,語法樹的雛形已經(jīng)建成。
語義解析
經(jīng)過了以上兩個(gè)步驟翅睛,解析完成的語法樹依舊不能進(jìn)入字節(jié)碼的編譯声搁,它還不夠完善。語義解析的任務(wù)就是將這個(gè)這顆不夠完善的語法樹擴(kuò)充地更加完善捕发。
語義解析步驟中經(jīng)歷的操作:
- 為沒有構(gòu)造方法的類型添加默認(rèn)的無參構(gòu)造函數(shù)
- 檢查任何變量是否在使用之前都被初始化過
- 檢查變量類型和值是不是匹配
- 將String類型和常量進(jìn)行合并處理
- 檢查代碼中的操作是不是都可達(dá)
- 異常檢查
- 將語法糖的內(nèi)容正呈柚迹化
常量折疊操作
如果一個(gè)String類型的數(shù)據(jù)是由多個(gè)常量通過『+』組成的,它其實(shí)只會(huì)創(chuàng)建一個(gè)String對象扎酷,編譯器在語義解析的時(shí)候檐涝,會(huì)將多個(gè)常量信息合并為一個(gè)對象。
//源代碼中的寫法
String str="Hello"+" "+"World!"
//經(jīng)過編譯器編譯之后
String str="Hello World!"
生成字節(jié)碼
在經(jīng)歷了一系列的語義解析之后法挨,所解析出來的語法樹就足夠完善了谁榜。這個(gè)時(shí)候編譯器最后的任務(wù)就是調(diào)用com.sun.tools.javac.jvm.Gen類,將這棵語法樹編譯為Java字節(jié)碼文件坷剧。
這個(gè)時(shí)候惰爬,符合Java規(guī)范的Java代碼就轉(zhuǎn)換成符合Java規(guī)范的字節(jié)碼文件了。