乐趣区

关于后端:深入浅出JVM六之前端编译过程与语法糖原理

本篇文章将围绕 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; // 编译谬误 标注查看 查看语法动态信息 
      2. 数据及控制流剖析: 对程序运行时动静查看

        • 比方办法中流程管制产生的各条路是否有适合的返回值
      3. 解语法糖: 将 (不便程序员应用的简洁代码) 语法糖转换为原始构造
      4. 字节码生成: 生成 <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));
  • 留神:

    1. 包装类重写的 equals 办法中不会主动转换类型
    2. 包装类的 == 就是去比拟援用地址,不会主动拆箱

条件编译

布尔类型 + 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 公布!

退出移动版