1. 程式人生 > >java編譯器原始碼分析之詞法分析器

java編譯器原始碼分析之詞法分析器

java編譯器是什麼?

編譯簡單理解就是一種高階語言到另一種低階語言的翻譯過程;而執行這個過程的主體稱為編譯器。尋常所說的編譯器是指把組合語言轉變成機器語言,也稱目的碼,即CPU指令集。組合語言是一種比機器語言對人友好的語言,但不同機器硬體構造不一樣,驅動機器的軟體也不一樣,因此組合語言需要針對不同的機器編寫不同的程式碼,顯得有點麻煩。為解決這個問題,聰明的工程師想到一種方法,即採用虛擬機器的形式遮蔽底層硬體和軟體平臺的不同,也就是說,高階語言的編寫不受底層硬體的影響,達到“一次編譯,到處執行”的效果。 很明顯,“一次編譯,到處執行”的功能寄託於虛擬機器;java語言是基於java虛擬機器(JVM)而實現的一種高階語言,它需要通過java編譯器編譯成JVM識別的語言,最後由JVM實現到目標語言的轉換。 javac

javac的作用?

javac是把java高階語言轉變成JVM識別的一種二進位制程式碼;具體體現就是.java檔案到.class檔案的轉變;JVM識別的.class檔案儲存的是位元組碼;而轉變的正確性則是由JVM的語言規範來保證,所以java編譯器的作用可以理解為把java語言規範轉變成JVM語言規範。

javac的主要過程?

從java語言到位元組碼的轉變要經過四個過程:①java語言到Token流的過程,稱為詞法分析;②Token流到抽象語法樹的過程,稱為語法分析;③解析複雜的樹節點,如語法糖的解析等,稱為語義分析;④抽象語法樹到位元組碼的過程,稱為程式碼生成。 javac過程 至於為什麼要分為這四個步驟?

抽象語法樹是關鍵。抽象語法樹可以把一種語言結構重組為另外一種語言結構,這裡可以簡單理解為java語言規範到JVM語言規範的轉變。

這篇部落格首先來看一下詞法分析的過程。

何為詞法分析?

詞法分析從字面來理解就是解析java語言中的單詞;單純的從字面來看,java檔案由java關鍵字、識別符號(包名、類名、屬性名和方法名)以及符號(各類運算子、各類括號)等三部分組成。詞法分析的主要目的就是把這些單詞和符號轉變成Token流。那麼什麼是Token流呢,後面會講到。

詞法分析的過程?

詞法分析在原始碼中是和語法分析在一起的,在這裡為了更好的理解詞法分析的過程,我是從程式碼的執行角度來分析的。有如下程式碼:

package com.compile;
public class CifaAnalysis {
	int i = 0;
	public static void main(String[] args) {
		System.out.println("Hello World!");
	}
}

大體過程如下: ①通過com.sun.tools.javac.main.Main類的compile()方法讀取.java檔案; ②通過com.sun.tools.javac.main.JavaCompiler.readSource(JavaFileObject)方法把檔案內容轉變成字元流(charSequence); ③通過com.sun.tools.javac.parser.Scanner.nextToken()方法從字元流中獲取一個token;

這裡有兩個問題: ①長串字元流如何形成一個個token?token長啥樣? ②形成的token存放在哪裡? 針對第一個問題,nextToken()方法說的很清楚,由於方法過長,這裡就不黏貼出來,簡單的說就是一個一個字元讀取字元流,比如”package com.compile”,當讀取p-a-c-k-a-g-e-空格,識別到空格時則會把空格前的字元流組成一個token字串.(Token是一個列舉類,列舉了所有的關鍵字、各類運算子和符號等)。或許你在這裡還有一個疑問,如果一個雙目運算子兩邊沒有空格如int i=0該如何形成四個token而不是兩個?在nextToken方法裡面已經對這種特殊操作符(!%&*?±:<=>|[email protected])進行了特殊的處理。

針對第二個問題,就要說說token是怎麼處理的了。 原來Token類裡面的name到value的對映是由com.sun.tools.javac.parser.Keywords.Keywords(Context)方法完成的,每一個token都以Name類的物件儲存在Token型別的陣列內;

key = new Token[maxKey+1];
for (int i = 0; i <= maxKey; i++) key[i] = IDENTIFIER;
for (Token t : Token.values()) {
      if (t.name != null)
          key[tokenName[t.ordinal()].index] = t;
}

maxKey表示Token列舉類的數量;通過第二個for迴圈把對映關係(name-value)初始化好,name沒有對映的value則value為IDENTIFIER;因此當nextToken()方法形成一個token時會定義為name物件,然後根據這個name物件去key陣列中查詢對應的value,沒有找到對應關係的name,其value為IDENTIFIER,所以com/compile等識別符號都會被定義為IDENTIFIER;

public Token key(Name name) {
        return (name.index > maxKey) ? IDENTIFIER : key[name.index];
    }

此時,針對Token放在哪裡的問題也就很清楚了。

就這樣,整個原檔案被形成下面這個樣子: 原始檔編譯 可以看出,經常誤以為的main和String並不是java關鍵字,只是作為一種IDENTIFIER。

最後還有一個問題,雖然我們知道package後面跟的是包名,但編譯器怎麼知道編譯器怎麼識別包名、類名、變數和方法名的? 編譯器內部針對Token.PACKAGE和Token.IMPORT都有單獨的邏輯處理: Token.PACKAGE處理

if (S.token() == PACKAGE) {
			if (mods != null) {
				checkNoMods(mods.flags);
				packageAnnotations = mods.annotations;
				mods = null;
			}
			S.nextToken();
			pid = qualident();
			accept(SEMI);
		}

Token.MONKEYS_AT處理

if (S.token() == MONKEYS_AT)
			mods = modifiersOpt();

Token.IMPORT處理

if (checkForImports && mods == null && S.token() == IMPORT) {
	/**
	* 解析import宣告
	*/
	defs.append(importDeclaration());
}

可知編譯器知道Token.PACKAGE後面跟的是包名,Token.IMPORT後面跟的是匯入的類名;Token.CLASS後面是類名等等這些都是事先約定好的,可以認為是java語言的規範。

總結

到此,詞法分析的整個過程結束了。可以整理下流程: ->讀取.java原始檔,並轉換為字元流; ->讀取字元流,根據規則形成name物件,並對映成Token; ->一個個Token形成Token流;

參考資料