從Java文件到字節(jié)碼文件

本文涉及的javac編譯器來自openjdk.

javac的目錄地址為:

解壓目錄/langtools/src/share/classes/com/sun/tools/javac/

javac編譯器將Java編譯成為一個有效的字節(jié)碼文件會經(jīng)歷4個步驟:

  • 詞法解析:將Java關(guān)鍵字排序,使得程序能有序運行。
  • 語法解析:詞法解析后的Token序列整合為一顆抽象的語法樹。
  • 語義解析:將抽象語法樹擴展地更加完善。
  • 字節(jié)碼解析:將字節(jié)碼解析成完整的類。

詞法解析


詞法解析是編譯器執(zhí)行的字節(jié)碼編譯的第一步。這個步驟中,將Java源碼中關(guān)鍵字和標識符等轉(zhuǎn)換成符合規(guī)范的Token序列。

詞法解析器的接口是com.sun.tools.javac.parser.Lexer ,它直接派生于同包下面的Scanner類,它的主要任務(wù)是按照單個字符的方式讀取Java源文件中的關(guān)鍵字標識符等,然后將其轉(zhuǎn)換為符合Java規(guī)范的Token序列。而負責(zé)詞法解析工作的是com.sun.tools.javac.parser.JavacParser類,該類的對象實例由ParseFactory負責(zé)創(chuàng)建,JavacParser負責(zé)詞法解析的具體細節(jié)。

當(dāng)我們在命令行敲入javac的時候,Java首先會調(diào)用com.sun.tools.javac.main.Main類的compile()方法。compile()方法接著就會調(diào)用JavaCompiler類的parseFile()方法,parseFile()的主要功能就是調(diào)用自己的parse()方法獲得JavacParser實例對象,然后調(diào)用JavacParser類的parseCompilationUnit()進行詞法解析。

這個過程如下圖所示:

語法解析過程

Token序列

Token其實就是一個枚舉類型,其內(nèi)部定了許多符合Java語法規(guī)范并與源碼字符集相對應(yīng)的枚舉常量。

所有的枚舉常量都在 com.sun.tools.javac.parser.Token類中。

編譯器在執(zhí)行詞法解析的過程中,只會對Token進行匹配校驗。

源碼字符集是如何轉(zhuǎn)換成Token的:

Name對象和Token對象建立的是一種一對一的關(guān)系。當(dāng)詞法解析器中需要將一個源碼字符集合解析成一個Token時,它會通過Names類調(diào)用Name類的fromChars()方法獲得一個Name對象,然后使用Keyswords類的key(Name name)方法獲得傳入相對應(yīng)的Token對象。

詞法解析器如何保存源碼字符集和Token之間的對應(yīng)關(guān)系:

詞法解析器在將源碼轉(zhuǎn)字符集合轉(zhuǎn)換為Token之前,會先將每一個字符集合都轉(zhuǎn)換成一個對應(yīng)的Name對象。接著再由com.sun.tools.javac.parser.Keywords類負責(zé)實際的Token轉(zhuǎn)換任務(wù)(將Token常量全部轉(zhuǎn)換為Name對象),然后轉(zhuǎn)換好的這些Name對象全部存到Name類的內(nèi)部類Table中,Keywords類中的數(shù)組key用于保存源碼字符集合和Token之間的對應(yīng)關(guān)系。

以上兩個問題略微有些復(fù)雜。畫了一個圖來表示一下:

源碼字符集和Token之間的對應(yīng)關(guān)系

調(diào)用nextToken()計算Token的獲取順序

Keywords類的key()方法僅僅是根據(jù)Name對象獲得對應(yīng)的Token,而詞法解析器是通過Scanner類的nextToken()方法保證Token的讀取順序規(guī)則。

調(diào)用parseCompilationUnit()方法執(zhí)行詞法解析

詞法解析的核心是校驗Token是否匹配com.sun.tools.javac.parser.JavacParser類在parseCompilationUnit()方法定義的匹配規(guī)則。parseCompilationUnit()方法會按照Token的匹配順序依次解析出package、import等關(guān)鍵字,當(dāng)這些Token匹配之后,詞法解析器會開始解析class主題信息,直到詞法解析全部結(jié)束,parseCompilationUnit()方法會將Token轉(zhuǎn)換為一棵結(jié)構(gòu)化的抽象語法樹。

語法解析


之前提過,語法解析的目的就是將經(jīng)過詞法解析得到的Token整合為一棵結(jié)構(gòu)化的抽象語法樹。

詞法解析完成的Token序列依舊還不完善,它們還沒有被整合起來,語法解析的主要任務(wù)是把這些零散的Token按照指定的Java語法規(guī)范整合起來形成一個有機的整體。

在語法解析階段,語法樹上每個節(jié)點都直接或者間接地繼承了JCTree類。

調(diào)用qualident()方法解析package語法節(jié)點

parseCompilationUnit()這個方法實際上,跨越了詞法解析和語法解析兩個階段。當(dāng)詞法解析器成功將package關(guān)鍵字聲明轉(zhuǎn)換為Token并完成詞法解析之后,會調(diào)用qualident()方法根據(jù)Token.PACKAGE解析為package語法節(jié)點。

語法解析步驟中,com.sun.tools.javac.tree.TreeMaker負責(zé)創(chuàng)建JCTree類的所有語法節(jié)點對象實例。所以TreeMaker本身就是一個語法解析器,不過具體的細節(jié)由parseCompilationUnit()方法來控制。

語法解析器實質(zhì)上還是使用Token對應(yīng)的Name對象,來作為轉(zhuǎn)換語法節(jié)點的素材。所以在解析語法樹之前,首先需要將Token轉(zhuǎn)換成對應(yīng)的Name對象。語法解析器就可以根據(jù)Name對象解析出一個JCIdent語法節(jié)點。

當(dāng)一個package關(guān)鍵字聲明中定義了多級目錄時,qualident()方法就會循環(huán)迭代調(diào)用語法解析器將package關(guān)鍵字聲明解析為嵌套的JCFieldAccess語法節(jié)點。

調(diào)用importDeclaration()方法解析import語法樹

當(dāng)成功解析出package語法節(jié)點之后,parseCompilationUnit()這個節(jié)點會調(diào)用importDeclaration()方法來解析得到import語法樹。

importDeclaration()首先匹配Token.Static來檢測import語句中是否包含了靜態(tài)導(dǎo)入。接著importDeclaration()就會調(diào)用語法解析器的Ident()方法解析出一個JCIdent語法節(jié)點,如果import語句中包含多級目錄的時候,語法解析器就會調(diào)用Select()方法解析為嵌套的JCFieldAccess語法節(jié)點。

當(dāng)語法解析器成功解析出JCIdent和JCFieldAccess節(jié)點之后,importDeclaration()方法會調(diào)用import()方法,將之前解析的語法節(jié)點,整合成為一棵JCImport節(jié)點。

實際開發(fā)中,通常會有多個import關(guān)鍵字聲明,那么importDeclaration()方法內(nèi)部會通過迭代循環(huán)方法解析出多個JCImport語法樹,然后將其存儲在一個集合中。

調(diào)用classDeclaration()方法解析class語法樹

語法解析的最后一步就是解析class的主體信息,當(dāng)語法解析器成功將import關(guān)鍵字解析聲明為JCIdent和JCFieldAccess語法節(jié)點并整合為一顆JCImport語法樹之后,parseCompilationUnit()方法內(nèi)部通過typeDeclaration()方法調(diào)用classOrInterfaceOrEnumDeclaration方法將class主體信息解析為一棵JCClassDecl語法樹。

當(dāng)以上這個過程結(jié)束之后,parseCompilationUnit()方法會調(diào)用TopLevel()方法將之前解析好過的package語法節(jié)點,import語法節(jié)點和class語法樹等內(nèi)容內(nèi)容信息全部整合成一棵JCCompilationUnit語法節(jié)點樹。

JCCompilationUnit類會成為整個語法樹的Root,持有整個語法樹的所有節(jié)點。這個時候,語法樹的雛形已經(jīng)建成。

語義解析


經(jīng)過了以上兩個步驟,解析完成的語法樹依舊不能進入字節(jié)碼的編譯,它還不夠完善。語義解析的任務(wù)就是將這個這顆不夠完善的語法樹擴充地更加完善。

語義解析步驟中經(jīng)歷的操作:

  • 為沒有構(gòu)造方法的類型添加默認的無參構(gòu)造函數(shù)
  • 檢查任何變量是否在使用之前都被初始化
  • 檢查變量類型和值是不是匹配
  • 將String類型和常量進行合并處理
  • 檢查代碼中的操作是不是都可達
  • 異常檢查
  • 將語法糖的內(nèi)容正?;?/li>

常量折疊操作

如果一個String類型的數(shù)據(jù)是由多個常量通過『+』組成的,它其實只會創(chuàng)建一個String對象,編譯器在語義解析的時候,會將多個常量信息合并為一個對象。

//源代碼中的寫法
String str="Hello"+" "+"World!"
//經(jīng)過編譯器編譯之后
String str="Hello World!"

生成字節(jié)碼


在經(jīng)歷了一系列的語義解析之后,所解析出來的語法樹就足夠完善了。這個時候編譯器最后的任務(wù)就是調(diào)用com.sun.tools.javac.jvm.Gen類,將這棵語法樹編譯為Java字節(jié)碼文件。

這個時候,符合Java規(guī)范的Java代碼就轉(zhuǎn)換成符合Java規(guī)范的字節(jié)碼文件了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容