本篇文章將圍繞Java中的編譯器,深入淺出的解析前端編譯的流程、泛型、條件編譯、增強for迴圈、可變長引數、lambda表示式等語法糖原理
編譯器與執行引擎
編譯器
Java中的編譯器不止一種,Java編譯器可以分為:前端編譯器、即時編譯器和提前編譯器
最為常見的就是前端編譯器javac,它能夠將Java原始碼編譯為位元組碼檔案,它能夠最佳化程式設計師使用起來很方便的語法糖
即時編譯器是在執行時,將熱點程式碼直接編譯為本地機器碼,而不需要解釋執行,提升效能
提前編譯器將程式提前編譯成本地二進位制程式碼
前端編譯過程
- 準備階段: 初始化插入式註解處理器
處理階段
解析與填充符號表
詞法分析: 將Java原始碼的字元流轉變為token(標記)流
- 字元: 程式編寫的最小單位
- 標記(token) : 編譯的最小單位
- 比如 關鍵字 static 是一個標記 / 6個字元
- 語法分析: 將token流構造成抽象語法樹
填充符號表: 產生符號資訊和符號地址
- 符號表是一組符號資訊和符號地址構成的資料結構
- 比如: 目的碼生成階段,對符號名分配地址時,要檢視符號表上該符號名對應的符號地址
插入式註解處理器的註解處理
註解處理器處理特殊註解: 在編譯器允許註解處理器對原始碼中特殊註解作處理,可以讀寫抽象語法樹中任意元素,如果發生了寫操作,就要重新解析填充符號表
- 比如: Lombok透過特殊註解,生成get/set/構造器等方法
語義分析與位元組碼生成
標註檢查: 對語義靜態資訊的檢查以及常量摺疊最佳化
int i = 1; char c1 = 'a'; int i2 = 1 + 2;//編譯成 int i2 = 3 常量摺疊最佳化 char c2 = i + c1; //編譯錯誤 標註檢查 檢查語法靜態資訊
資料及控制流分析: 對程式執行時動態檢查
- 比如方法中流程控制產生的各條路是否有合適的返回值
- 解語法糖: 將(方便程式設計師使用的簡潔程式碼)語法糖轉換為原始結構
- 位元組碼生成: 生成
<init>,<clinit>
方法,並根據上述資訊生成位元組碼檔案
前端編譯流程圖
原始碼分析
程式碼位置在JavaCompiler的compile方法中
Java中的語法糖
泛型
將操作的資料型別指定為方法簽名中一種特殊引數,作用在方法、類、介面上時稱為泛型方法、泛型類、泛型介面
Java中的泛型是型別擦除式泛型,泛型只在原始碼中存在,在編譯期擦除泛型,並在相應的地方加上強制轉換程式碼
與具現化式泛型(不會擦除,執行時也存在泛型)對比
優點: 只需要改動編譯器,Java虛擬機器和位元組碼指令不需要改變
- 因為泛型是JDK5加入的,為了滿足對以前版本程式碼的相容採用型別擦除式泛型
缺點: 效能較低,使用沒那麼方便
- 為提供基本型別的泛型,只能自動拆裝箱,在相應的地方還會加速強制轉換程式碼,所以效能較低
執行期間無法獲取到泛型型別資訊
比如書寫泛型的List轉陣列型別時,需要在方法的引數中指定泛型型別
public static <T> T[] listToArray(List<T> list,Class<T> componentType){ T[] instance = (T[]) Array.newInstance(componentType, list.size()); return instance; }
增強for迴圈與可變長引數
增強for迴圈 -> 迭代器
可變長引數 -> 陣列裝載引數
泛型擦除後會在某些位置插入強制轉換程式碼
自動拆裝箱
自動裝箱、拆箱的錯誤用法
Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
Long g = 3L;
//true
System.out.println(c == d);//範圍小,在緩衝池中
//false
System.out.println(e == f);//範圍大,不在緩衝池中,比較地址因此為false
//true
System.out.println(c == (a + b));
//true
System.out.println(c.equals(a + b));
//false
System.out.println(g == (b + a));
//true
System.out.println(g.equals(a + b));
注意:
- 包裝類重寫的equals方法中不會自動轉換型別
- 包裝類的 == 就是去比較引用地址,不會自動拆箱
- 包裝類重寫的equals方法中不會自動轉換型別
條件編譯
布林型別 + if語句 : 根據布林值型別的真假,編譯器會把分支中不成立的程式碼塊消除(解語法糖)
Lambda原理
編寫函式式介面
@FunctionalInterface
interface LambdaTest {
void lambda();
}
編寫測試類
public class Lambda {
private int i = 10;
public static void main(String[] args) {
test(() -> System.out.println("匿名內部類實現函式式介面"));
}
public static void test(LambdaTest lambdaTest) {
lambdaTest.lambda();
}
}
使用外掛檢視位元組碼檔案
生成了一個私有靜態的方法,這個方法中很明顯就是lambda中的程式碼
在使用lambda表示式的類中隱式生成一個靜態私有的方法,這個方法程式碼塊就是lambda表示式中寫的程式碼
執行class檔案時帶上引數java -Djdk.internal.lambda.dumpProxyClasses 包名.類名
即可顯示出這個匿名內部類
使用invokedynamic
生成了一個實現函式式介面的匿名內部類物件,在重寫函式式介面的方法實現中呼叫使用lambda表示式類中隱式生成的靜態私有方法
總結
本篇文章以Java中編譯器的分類為開篇,深入淺出的解析前端編譯的流程,Java中泛型、增強for迴圈、可變長引數、自動拆裝箱、條件編譯以及Lambda等語法糖的原理
前端編譯先將字元流轉換為token流,再將token流轉換為抽象語法樹,填充符號表的符號資訊、符號地址,然後註解處理器處理特殊註解(比如Lombok生成get、set方法),對語法樹發生寫改動則要重新解析、填充符號,接著檢查語義靜態資訊以及常量摺疊,對執行時程式進行動態檢查,再解語法糖,生成init例項方法、clinit靜態方法,最後生成位元組碼檔案
Java中為了相容之前的版本使用型別擦除式的泛型,在編譯期間擦除泛型並在相應位置加上強制轉換,想為基本型別使用泛型只能搭配自動拆裝箱一起使用,效能有損耗且在執行時無法獲取泛型型別
增加for迴圈則是使用迭代器實現,並在適當位置插入強制轉換;可變長引數則是建立陣列進行裝載引數
自動拆裝箱提供基本型別與包裝類的轉換,但包裝類儘量不使用==,這是去比較引用地址,同型別比較使用equals
條件編譯會在if-else語句中根據布林型別將不成立的分支程式碼塊消除
lambda原理則是透過invokeDynamic
指令動態生成實現函式式介面的匿名物件,匿名物件重寫函式時介面方法中呼叫使用lambda表示式類中隱式生成的靜態私有的方法(該方法就是lambda表示式中的程式碼內容)
最後(不要白嫖,一鍵三連求求拉\~)
本篇文章筆記以及案例被收入 gitee-StudyJava、 github-StudyJava 感興趣的同學可以stat下持續關注喔\~
有什麼問題可以在評論區交流,如果覺得菜菜寫的不錯,可以點贊、關注、收藏支援一下\~
關注菜菜,分享更多幹貨,公眾號:菜菜的後端私房菜
本文由部落格一文多發平臺 OpenWrite 釋出!