一、簡單嘮嘮JAVAC
javac這個命令,搞java的都不陌生,很多人在第一次用java輸出hello world時,都接觸過這個命令,它的作用簡單來說,就是將指定的java源代碼編譯成class文件,后來我們有了Eclipse等IDE工具,就很少關心這個命令了,偶爾需要的時候,才想起來javac xx.java一下,其實我也一樣。我也認為javac命令很簡單,不就是編譯個class文件嗎。但是隨著對jvm的了解,我才覺得,jdk中提供的很多命令都是很有意思的。javac這個命令也遠沒有我想想的那么簡單。
比如,我在介紹javap命令時,如果對直接使用javac編譯的class文件,進行javap xxx查看其反匯編信息時,看不到方法的局部變量表、代碼行與指令行的偏移映射表等信息。再比如,jdk8之前,并沒有提供通過反射獲得方法中入參的參數名的api(如何方法入參參數名,可以參考我的文章)等等。
接下來,我就根據我查到的資料和感悟來簡單的說說javac命令。
二、JAVAC命令的工作過程
我們都知道javac命令的作用是將java源碼編譯成二進制字節碼class文件,那么從java源文件編譯成class文件這個過程中JAVAC命令都進行了什么操作呢,或者說JAVAC命令的工作過程是什么樣的呢?
首先,來看看編譯原理(上學時學過,現在基本都還給老師了,網上查了查資料)中編譯過程主要經歷以下階段:
javac命令在進行編譯操作時,也會按照類似的過程進行:
(1)詞法分析階段
詞法分析,就是將獲得的java源代碼信息轉化為標記(Token)集合,比如關鍵字、變量、運算符等等,都是一個個的標記。詞法分析的過程就是將這些標記解析出來。
(2)語法分析階段
語法分析是在詞法分析得到的標記集合的基礎上,抽象出對應的語法樹。什么是語法樹呢?簡單的來說,一個java源文件中包信息,import信息、類定義信息、方法信息、字段信息等待作為一個個的項,這些項集合在一起就抽象為一棵語法樹。
為了更直觀的解釋什么是語法樹,這里做個測試,首先在eclipse中創建一個Test1.java文件,在這個java文件中添加兩個類Test1和Test2,內容如下:
package com.test.map;
import java.util.Date;
public class Test1 {
private String name;
public static final int age = 20;
public void test(String username){
this.name = username;
System.out.println(new Date());
}
public void test2(int a){
try {
System.out.println(a/0);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
class Test2{
public void tst(){
}
}
然后使用EClipse的AST插件(AST插件安裝)分析當前java源碼的語法樹,內容如下,一目了然,很清晰,包含了這個java文件package信息、import引入的依賴信息,定義的類,類的字段和方法等等信息:
(3)符號表填充階段
符號表(SymbolTable)是由一組符號地址和符號信息構成的表格,可以把它想象成哈希表中K-V值對的形式(實際上符號表不一定是哈希表實現,可以是有序符號表、樹狀符號表、棧結構符號表等)。符號表中所登記的信息在編譯的不同階段都要用到。在語義分析中,符號表所登記的內容將用于語義檢查(如檢類型是否匹配等)和產生中間代碼。在目標代碼生成階段,當對符號名進行地址分配時,符號表是地址分配的依據。
(4)注解處理階段
在jdk1.5之后,java提供了對注解的支持,注解可以在編譯、類加載、運行時被讀取,并執行相應的處理。通過使用注解,開發人員可以在不改變原有邏輯的情況下,在源文件中嵌入一些補充信息。如果有需要在編譯期間被處理的注解,則這些注解將會在當前階段進行讀取和處理。
(5)語義分析
在語法分析之后,編譯器獲得了程序源碼的抽象語法樹,語法樹能表示一個結構正確的源程序的抽象,但無法保證源程序是符合邏輯的。而語義分析的主要任務是對結構上正確的源程序進行上下文有關性質的審查,如進行類型審查。舉個例子,假設有如下的3個變量定義語句:
int a = 1;
boolean b = false;
char c = 3;
后續的代碼中可能出現操作賦值運行:
int d = a+c;
int e = b+c;
char f = a+b;
上面的代碼中,它們都能構成結構正確的語法樹,但是只有第1種的寫法在語義上是沒有問題的,能夠通過編譯,其余兩種在Java語言中是不合邏輯的,無法編譯(在java中int類型可以和char、short、byte類型進行加減等操作,但不能和boolean進行相關操作)。
javac的編譯過程中,語義分析過程分為標注檢查、數據及控制流分析、解除語法糖3個步驟。
1、標注檢查
標注檢查,主要包括諸如變量使用前是否已被聲明、變量與賦值之間的數據類型是否能夠匹配等。在標注檢查步驟中,還有一個重要的動作稱為常量折疊,如果我們在代碼中寫了如下定義:
int a = 1+2;
那么在語法樹上仍然能看到字面量“1”、“2”以及操作符“+”,但是在經過語義分析階段的常量折疊之后,它們將會被折疊為字面常量“3”。
2、數據及控制流分析
數據及控制流分析是對程序上下文邏輯更進一步的驗證,它可以檢查出諸如程序局部變量在使用前是否有賦值、方法的每條路徑是否都有返回值、是否所有的受查異常都被正確處理了等問題。編譯時期的數據及控制流分析與類加載時的數據及控制流分析的目的基本上是一致的,但校驗范圍有所區別,有一些校驗項只有在編譯期或運行期才能進行。
3、解除語法糖
語法糖,簡單的來說,就是在開發語言中添加某些語法,這些語法對開發人員是非常友好和有用的,主要用來使用的開發語義更易用、開發人員開發出的代碼有更好的可讀性、減少程序代碼出錯率等。但是這些語法對開發語言的功能和性能并沒有太大的影響。
舉個例子,java中基本類型和其對應引用類型直接的拆箱裝箱操作,在我們的代碼中我們可以這樣寫:
int a = 1;
Integer b = a+2;
實際上,在編譯之后,通過gui等class反編譯工具,可以看到:
int a = 1;
Integer b = Integer.valueOf(a+2);
在java中,還提供了for(i:xx)循環遍歷、支持string的switch case、泛型、變長參數等等語法糖。關于java中的語法糖,后續會出一篇博客,專門講述。
解除語法糖階段,就是將我們代碼中java提供的語法糖解析還原為java原本的基礎語法結構。因為在運行期間,jvm是不支持這些語法糖對應的語法的。
(6)字節碼生成階段
字節碼生成階段,會將前面生成的語法樹、符號表等信息轉化為字節碼輸出到磁盤中,并且會進行相關的代碼添加和轉換工作。
比如,當我們使用javap查看反匯編代碼時,會看到通過new創建對象時,實際上是調用了這個對象的<init>方法,完成對象的初始化,這個<init>方法就是在字節碼生成階段添加的,它會將我們在代碼中寫的普通語句塊、成員變量初始化、調用父類構造器等等操作都放入到<init>方法中,完成對象的初始化操作。
再比如,多個字符串變量相加a+b+c,實際上是創建了一個StringBuilder對象,對這些字符串變量進行append()操作,這些通過javap都能看到。
關于javap的使用,可以參考我的博客《通過javap命令分析java匯編指令》。
以上,就是javac命令工作的過程,下面講述一下javac命令的使用。
三、JAVAC命令的使用
一般安裝好jdk后,我們會在控制臺執行javac命令,以驗證jdk是否安裝成功,比如,我在我本地執行javac,會輸出一下內容:
用法: javac <options> <source files>
其中, 可能的選項包括:
-g 生成所有調試信息
-g:none 不生成任何調試信息
-g:{lines,vars,source} 只生成某些調試信息
-nowarn 不生成任何警告
-verbose 輸出有關編譯器正在執行的操作的消息
-deprecation 輸出使用已過時的 API 的源位置
-classpath <路徑> 指定查找用戶類文件和注釋處理程序的位置
-cp <路徑> 指定查找用戶類文件和注釋處理程序的位置
-sourcepath <路徑> 指定查找輸入源文件的位置
-bootclasspath <路徑> 覆蓋引導類文件的位置
-extdirs <目錄> 覆蓋所安裝擴展的位置
-endorseddirs <目錄> 覆蓋簽名的標準路徑的位置
-proc:{none,only} 控制是否執行注釋處理和/或編譯。
-processor <class1>[,<class2>,<class3>...] 要運行的注釋處理程序的名稱; 繞過默認的搜索進程
-processorpath <路徑> 指定查找注釋處理程序的位置
-d <目錄> 指定放置生成的類文件的位置
-s <目錄> 指定放置生成的源文件的位置
-implicit:{none,class} 指定是否為隱式引用文件生成類文件
-encoding <編碼> 指定源文件使用的字符編碼
-source <發行版> 提供與指定發行版的源兼容性
-target <發行版> 生成特定 VM 版本的類文件
-version 版本信息
-help 輸出標準選項的提要
-A關鍵字[=值] 傳遞給注釋處理程序的選項
-X 輸出非標準選項的提要
-J<標記> 直接將 <標記> 傳遞給運行時系統
-Werror 出現警告時終止編譯
@<文件名> 從文件讀取選項和文件名
直接執行javac命令,就等同執行javac -help,它會輸出javac這個命令的使用規則,以及可以使用的相關參數及參數的簡介。
3.1、JAVAC命令的使用格式
在前面執行javac輸出的信息中,我們看到其輸出的用法為:
javac <options> <source files>
其中:
options表示我們在使用javac命令時需要指定的參數選項,可以同時指定多個參數,每個參數之間使用空格隔開。參數選項在javac的使用說明中都列出來了,下一小節,會著重講解其中的幾個參數。
source files表示我們要編譯的java源碼文件。可以是多個文件,使用空格隔開。并且,這些文件文件名都必須以.java結尾。
比如:
javac -nowarn,-verbose Test.java Test2.java
另外,javac后面的參數、文件等信息并沒有固定的順序,你可以按照自己的意愿隨便指定各個參數和文件信息的位置順序,比如:
javac -nowarn Test1.java -verbose
還有一點需要注意:
在前面javac輸出的信息中,最后一項:
@<文件名> 從文件讀取選項和文件名
意思是,你可以將參數選項,文件信息單獨放到一個或多個文件中,然后執行:
javac @xxx文件就可以在執行javac命令時,將xxx文件中的內容傳遞給javac命令。
例如,有兩個java文件Test1.java,Test2.java。然后新建一個classmsgs.txt文件,文件內容為:
Test1.java Test2.java -verbose
然后執行下面的命令:
javac -nowarn @classmsg.txt
如果有多個文件,可以使用空格隔開,如:
javac -nowarn @classmsgs.txt @classmsgs2.txt
就可以看到,Test1和Test2被編譯成class文件,并且在編譯時會輸出編譯器正在執行的操作日志(因為使用了-verbose參數)。
一般情況下,當你需要編譯的java文件比較多,或者需要設置的參數比較多時,亦或者要復用一些參數信息時,可以將這些java文件名或者參數項抽取出來,放到一個或多個文件中。這一點很像spring中xml的import,css中@等在一個文件中引入其他文件的方式。看來軟件開發中,很多地方都是想通的。
綜上,javac的實際使用格式可以歸納為:
javac <options> <source files> @files
3.2、JAVAC命令中的參數項
在前面直接指向javac命令輸出的信息中,我們看到javac用到的參數項有很多,那么這些參數項都是用來干什么的得呢?這里我按照自己查閱的資料以及自己的實驗和理解,做一下講解。這些參數項很多,我將它們分為一下幾類:
先寫到這里,先挖個坑,后續再填。