深入淺出JVM(六)之前端編譯過程與語法糖原理

發表於2024-02-24

本篇文章將圍繞Java中的編譯器,深入淺出的解析前端編譯的流程、泛型、條件編譯、增強for迴圈、可變長引數、lambda表示式等語法糖原理

編譯器與執行引擎

編譯器

Java中的編譯器不止一種,Java編譯器可以分為:前端編譯器、即時編譯器和提前編譯器

最為常見的就是前端編譯器javac,它能夠將Java原始碼編譯為位元組碼檔案,它能夠最佳化程式設計師使用起來很方便的語法糖

即時編譯器是在執行時,將熱點程式碼直接編譯為本地機器碼,而不需要解釋執行,提升效能

提前編譯器將程式提前編譯成本地二進位制程式碼

前端編譯過程

  • 準備階段: 初始化插入式註解處理器
  • 處理階段

    • 解析與填充符號表

      1. 詞法分析: 將Java原始碼的字元流轉變為token(標記)流

        • 字元: 程式編寫的最小單位
        • 標記(token) : 編譯的最小單位
        • 比如 關鍵字 static 是一個標記 / 6個字元
      2. 語法分析: 將token流構造成抽象語法樹
      3. 填充符號表: 產生符號資訊和符號地址

        • 符號表是一組符號資訊和符號地址構成的資料結構
        • 比如: 目的碼生成階段,對符號名分配地址時,要檢視符號表上該符號名對應的符號地址
    • 插入式註解處理器的註解處理

      1. 註解處理器處理特殊註解: 在編譯器允許註解處理器對原始碼中特殊註解作處理,可以讀寫抽象語法樹中任意元素,如果發生了寫操作,就要重新解析填充符號表

        • 比如: Lombok透過特殊註解,生成get/set/構造器等方法
    • 語義分析與位元組碼生成

      1. 標註檢查: 對語義靜態資訊的檢查以及常量摺疊最佳化

         int i = 1;
         char c1 = 'a';
         int i2 = 1 + 2;//編譯成 int i2 = 3 常量摺疊最佳化
         char c2 = i + c1; //編譯錯誤 標註檢查 檢查語法靜態資訊 

        image-20210524202623150.png

      2. 資料及控制流分析: 對程式執行時動態檢查

        • 比如方法中流程控制產生的各條路是否有合適的返回值
      3. 解語法糖: 將(方便程式設計師使用的簡潔程式碼)語法糖轉換為原始結構
      4. 位元組碼生成: 生成<init>,<clinit>方法,並根據上述資訊生成位元組碼檔案
前端編譯流程圖

image-20210524205803664.png

原始碼分析

image-20210524222754508.png
程式碼位置在JavaCompiler的compile方法中

image-20210524221445424.png

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迴圈與可變長引數

image-20210524213429033.png

增強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));
  • 注意:

    1. 包裝類重寫的equals方法中不會自動轉換型別
      image-20210524213853321.png
    2. 包裝類的 == 就是去比較引用地址,不會自動拆箱

條件編譯

布林型別 + if語句 : 根據布林值型別的真假,編譯器會把分支中不成立的程式碼塊消除(解語法糖)

image-20210524214427206.png

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();
     }
 }
使用外掛檢視位元組碼檔案

image-20210524230643123.png
生成了一個私有靜態的方法,這個方法中很明顯就是lambda中的程式碼

在使用lambda表示式的類中隱式生成一個靜態私有的方法,這個方法程式碼塊就是lambda表示式中寫的程式碼

image-20210524232010510.png
執行class檔案時帶上引數java -Djdk.internal.lambda.dumpProxyClasses 包名.類名即可顯示出這個匿名內部類

image-20210527083659256.png

使用invokedynamic生成了一個實現函式式介面的匿名內部類物件,在重寫函式式介面的方法實現中呼叫使用lambda表示式類中隱式生成的靜態私有方法

總結

本篇文章以Java中編譯器的分類為開篇,深入淺出的解析前端編譯的流程,Java中泛型、增強for迴圈、可變長引數、自動拆裝箱、條件編譯以及Lambda等語法糖的原理

前端編譯先將字元流轉換為token流,再將token流轉換為抽象語法樹,填充符號表的符號資訊、符號地址,然後註解處理器處理特殊註解(比如Lombok生成get、set方法),對語法樹發生寫改動則要重新解析、填充符號,接著檢查語義靜態資訊以及常量摺疊,對執行時程式進行動態檢查,再解語法糖,生成init例項方法、clinit靜態方法,最後生成位元組碼檔案

Java中為了相容之前的版本使用型別擦除式的泛型,在編譯期間擦除泛型並在相應位置加上強制轉換,想為基本型別使用泛型只能搭配自動拆裝箱一起使用,效能有損耗且在執行時無法獲取泛型型別

增加for迴圈則是使用迭代器實現,並在適當位置插入強制轉換;可變長引數則是建立陣列進行裝載引數

自動拆裝箱提供基本型別與包裝類的轉換,但包裝類儘量不使用==,這是去比較引用地址,同型別比較使用equals

條件編譯會在if-else語句中根據布林型別將不成立的分支程式碼塊消除

lambda原理則是透過invokeDynamic指令動態生成實現函式式介面的匿名物件,匿名物件重寫函式時介面方法中呼叫使用lambda表示式類中隱式生成的靜態私有的方法(該方法就是lambda表示式中的程式碼內容)

最後(不要白嫖,一鍵三連求求拉\~)

本篇文章筆記以及案例被收入 gitee-StudyJavagithub-StudyJava 感興趣的同學可以stat下持續關注喔\~

有什麼問題可以在評論區交流,如果覺得菜菜寫的不錯,可以點贊、關注、收藏支援一下\~

關注菜菜,分享更多幹貨,公眾號:菜菜的後端私房菜

本文由部落格一文多發平臺 OpenWrite 釋出!

相關文章