本文涉及的javac編譯器來自openjdk.
javac的目錄地址爲:解壓目錄/langtools/src/share/classes/com/sun/tools/javac/
javac編譯器將Java編譯成爲一個有效的字節碼文件會經歷4個步驟:
- 詞法解析:將Java關鍵字排序,使得程序能有序運行。
- 語法解析:詞法解析後的Token序列整合爲一顆抽象的語法樹。
- 語義解析:將抽象語法樹擴展地更加完善。
- 字節碼解析:將字節碼解析成完整的類。
詞法解析
詞法解析是編譯器執行的字節碼編譯的第一步。這個步驟中,將Java源碼中關鍵字和標識符等轉換成符合規範的Token序列。
詞法解析器的接口是com.sun.tools.javac.parser.Lexer ,它直接派生於同包下面的Scanner類,它的主要任務是按照單個字符的方式讀取Java源文件中的關鍵字和標識符等,然後將其轉換爲符合Java規範的Token序列。而負責詞法解析工作的是com.sun.tools.javac.parser.JavacParser類,該類的對象實例由ParseFactory負責創建,JavacParser負責詞法解析的具體細節。
當我們在命令行敲入javac的時候,Java首先會調用com.sun.tools.javac.main.Main類的compile()方法。compile()方法接着就會調用JavaCompiler類的parseFile()方法,parseFile()的主要功能就是調用自己的parse()方法獲得JavacParser實例對象,然後調用JavacParser類的parseCompilationUnit()進行詞法解析。
這個過程如下圖所示:
Token序列
Token其實就是一個枚舉類型,其內部定了許多符合Java語法規範並與源碼字符集相對應的枚舉常量。
所有的枚舉常量都在 com.sun.tools.javac.parser.Token類中。
編譯器在執行詞法解析的過程中,只會對Token進行匹配校驗。
源碼字符集是如何轉換成Token的:
Name對象和Token對象建立的是一種一對一的關係。當詞法解析器中需要將一個源碼字符集合解析成一個Token時,它會通過Names類調用Name類的fromChars()方法獲得一個Name對象,然後使用Keyswords類的key(Name name)方法獲得傳入相對應的Token對象。
詞法解析器如何保存源碼字符集和Token之間的對應關係:
詞法解析器在將源碼轉字符集合轉換爲Token之前,會先將每一個字符集合都轉換成一個對應的Name對象。接着再由com.sun.tools.javac.parser.Keywords類負責實際的Token轉換任務(將Token常量全部轉換爲Name對象),然後轉換好的這些Name對象全部存到Name類的內部類Table中,Keywords類中的數組key用於保存源碼字符集合和Token之間的對應關係。
以上兩個問題略微有些複雜。畫了一個圖來表示一下:
調用nextToken()計算Token的獲取順序
Keywords類的key()方法僅僅是根據Name對象獲得對應的Token,而詞法解析器是通過Scanner類的nextToken()方法保證Token的讀取順序規則。
調用parseCompilationUnit()方法執行詞法解析
詞法解析的核心是校驗Token是否匹配com.sun.tools.javac.parser.JavacParser類在parseCompilationUnit()方法定義的匹配規則。parseCompilationUnit()方法會按照Token的匹配順序依次解析出package、import等關鍵字,當這些Token匹配之後,詞法解析器會開始解析class主題信息,直到詞法解析全部結束,parseCompilationUnit()方法會將Token轉換爲一棵結構化的抽象語法樹。
語法解析
之前提過,語法解析的目的就是將經過詞法解析得到的Token整合爲一棵結構化的抽象語法樹。
詞法解析完成的Token序列依舊還不完善,它們還沒有被整合起來,語法解析的主要任務是把這些零散的Token按照指定的Java語法規範整合起來形成一個有機的整體。
在語法解析階段,語法樹上每個節點都直接或者間接地繼承了JCTree類。
調用qualident()方法解析package語法節點
parseCompilationUnit()這個方法實際上,跨越了詞法解析和語法解析兩個階段。當詞法解析器成功將package關鍵字聲明轉換爲Token並完成詞法解析之後,會調用qualident()方法根據Token.PACKAGE解析爲package語法節點。
語法解析步驟中,com.sun.tools.javac.tree.TreeMaker負責創建JCTree類的所有語法節點對象實例。所以TreeMaker本身就是一個語法解析器,不過具體的細節由parseCompilationUnit()方法來控制。
語法解析器實質上還是使用Token對應的Name對象,來作爲轉換語法節點的素材。所以在解析語法樹之前,首先需要將Token轉換成對應的Name對象。語法解析器就可以根據Name對象解析出一個JCIdent語法節點。
當一個package關鍵字聲明中定義了多級目錄時,qualident()方法就會循環迭代調用語法解析器將package關鍵字聲明解析爲嵌套的JCFieldAccess語法節點。
調用importDeclaration()方法解析import語法樹
當成功解析出package語法節點之後,parseCompilationUnit()這個節點會調用importDeclaration()方法來解析得到import語法樹。
importDeclaration()首先匹配Token.Static來檢測import語句中是否包含了靜態導入。接着importDeclaration()就會調用語法解析器的Ident()方法解析出一個JCIdent語法節點,如果import語句中包含多級目錄的時候,語法解析器就會調用Select()方法解析爲嵌套的JCFieldAccess語法節點。
當語法解析器成功解析出JCIdent和JCFieldAccess節點之後,importDeclaration()方法會調用import()方法,將之前解析的語法節點,整合成爲一棵JCImport節點。
實際開發中,通常會有多個import關鍵字聲明,那麼importDeclaration()方法內部會通過迭代循環方法解析出多個JCImport語法樹,然後將其存儲在一個集合中。
調用classDeclaration()方法解析class語法樹
語法解析的最後一步就是解析class的主體信息,當語法解析器成功將import關鍵字解析聲明爲JCIdent和JCFieldAccess語法節點並整合爲一顆JCImport語法樹之後,parseCompilationUnit()方法內部通過typeDeclaration()方法調用classOrInterfaceOrEnumDeclaration方法將class主體信息解析爲一棵JCClassDecl語法樹。
當以上這個過程結束之後,parseCompilationUnit()方法會調用TopLevel()方法將之前解析好過的package語法節點,import語法節點和class語法樹等內容內容信息全部整合成一棵JCCompilationUnit語法節點樹。
JCCompilationUnit類會成爲整個語法樹的Root,持有整個語法樹的所有節點。這個時候,語法樹的雛形已經建成。
語義解析
經過了以上兩個步驟,解析完成的語法樹依舊不能進入字節碼的編譯,它還不夠完善。語義解析的任務就是將這個這顆不夠完善的語法樹擴充地更加完善。
語義解析步驟中經歷的操作:
- 爲沒有構造方法的類型添加默認的無參構造函數
- 檢查任何變量是否在使用之前都被初始化過
- 檢查變量類型和值是不是匹配
- 將String類型和常量進行合併處理
- 檢查代碼中的操作是不是都可達
- 異常檢查
- 將語法糖的內容正常化
常量摺疊操作
如果一個String類型的數據是由多個常量通過『+』組成的,它其實只會創建一個String對象,編譯器在語義解析的時候,會將多個常量信息合併爲一個對象。
//源代碼中的寫法
String str="Hello"+" "+"World!"
//經過編譯器編譯之後
String str="Hello World!"
生成字節碼
在經歷了一系列的語義解析之後,所解析出來的語法樹就足夠完善了。這個時候編譯器最後的任務就是調用com.sun.tools.javac.jvm.Gen類,將這棵語法樹編譯爲Java字節碼文件。
這個時候,符合Java規範的Java代碼就轉換成符合Java規範的字節碼文件了。