深入浅出JVM(六)之前端编译过程与语法糖原理

简介: 深入浅出JVM(六)之前端编译过程与语法糖原理

本篇文章将围绕Java中的编译器,深入浅出的解析前端编译的流程、泛型、条件编译、增强for循环、可变长参数、lambda表达式等语法糖原理

编译器与执行引擎

编译器

Java中的编译器不止一种,Java编译器可以分为:前端编译器、即时编译器和提前编译器

最为常见的就是前端编译器javac,它能够将Java源代码编译为字节码文件,它能够优化程序员使用起来很方便的语法糖

即时编译器是在运行时,将热点代码直接编译为本地机器码,而不需要解释执行,提升性能

提前编译器将程序提前编译成本地二进制代码

前端编译过程

  • 准备阶段: 初始化插入式注解处理器
  • 处理阶段
  • 解析与填充符号表
  1. 词法分析: 将Java源代码的字符流转变为token(标记)流
  • 字符: 程序编写的最小单位
  • 标记(token) : 编译的最小单位
  • 比如 关键字 static 是一个标记 / 6个字符
  1. 语法分析: 将token流构造成抽象语法树
  2. 填充符号表: 产生符号信息和符号地址
  • 符号表是一组符号信息和符号地址构成的数据结构
  • 比如: 目标代码生成阶段,对符号名分配地址时,要查看符号表上该符号名对应的符号地址
  • 插入式注解处理器的注解处理
  1. 注解处理器处理特殊注解: 在编译器允许注解处理器对源代码中特殊注解作处理,可以读写抽象语法树中任意元素,如果发生了写操作,就要重新解析填充符号表
  • 比如: Lombok通过特殊注解,生成get/set/构造器等方法
  • 语义分析与字节码生成
  1. 标注检查: 对语义静态信息的检查以及常量折叠优化
 int i = 1;
 char c1 = 'a';
 int i2 = 1 + 2;//编译成 int i2 = 3 常量折叠优化
 char c2 = i + c1; //编译错误 标注检查 检查语法静态信息 

image.png

  1. 数据及控制流分析: 对程序运行时动态检查
  • 比如方法中流程控制产生的各条路是否有合适的返回值
  1. 解语法糖: 将(方便程序员使用的简洁代码)语法糖转换为原始结构
  2. 字节码生成: 生成<init>,<clinit>方法,并根据上述信息生成字节码文件

前端编译流程图

image.png

源码分析

image.png

代码位置在JavaCompiler的compile方法中

image.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.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.png

  1. 包装类的 == 就是去比较引用地址,不会自动拆箱

条件编译

布尔类型 + if语句 : 根据布尔值类型的真假,编译器会把分支中不成立的代码块消除(解语法糖)

image.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.png

生成了一个私有静态的方法,这个方法中很明显就是lambda中的代码

在使用lambda表达式的类中隐式生成一个静态私有的方法,这个方法代码块就是lambda表达式中写的代码

image.png

执行class文件时带上参数java -Djdk.internal.lambda.dumpProxyClasses 包名.类名即可显示出这个匿名内部类

image.png

使用invokedynamic生成了一个实现函数式接口的匿名内部类对象,在重写函数式接口的方法实现中调用使用lambda表达式类中隐式生成的静态私有方法

总结

本篇文章以Java中编译器的分类为开篇,深入浅出的解析前端编译的流程,Java中泛型、增强for循环、可变长参数、自动拆装箱、条件编译以及Lambda等语法糖的原理

前端编译先将字符流转换为token流,再将token流转换为抽象语法树,填充符号表的符号信息、符号地址,然后注解处理器处理特殊注解(比如Lombok生成get、set方法),对语法树发生写改动则要重新解析、填充符号,接着检查语义静态信息以及常量折叠,对运行时程序进行动态检查,再解语法糖,生成init实例方法、clinit静态方法,最后生成字节码文件

Java中为了兼容之前的版本使用类型擦除式的泛型,在编译期间擦除泛型并在相应位置加上强制转换,想为基本类型使用泛型只能搭配自动拆装箱一起使用,性能有损耗且在运行时无法获取泛型类型

增加for循环则是使用迭代器实现,并在适当位置插入强制转换;可变长参数则是创建数组进行装载参数

自动拆装箱提供基本类型与包装类的转换,但包装类尽量不使用==,这是去比较引用地址,同类型比较使用equals

条件编译会在if-else语句中根据布尔类型将不成立的分支代码块消除

lambda原理则是通过invokeDynamic指令动态生成实现函数式接口的匿名对象,匿名对象重写函数时接口方法中调用使用lambda表达式类中隐式生成的静态私有的方法(该方法就是lambda表达式中的代码内容)


相关文章
|
1月前
|
存储 前端开发 JavaScript
第六章(原理篇) 微前端间的通信机制
第六章(原理篇) 微前端间的通信机制
|
1月前
|
资源调度 监控 前端开发
第七章(原理篇) 微前端技术之依赖管理与版本控制
第七章(原理篇) 微前端技术之依赖管理与版本控制
|
1月前
|
前端开发 JavaScript UED
第五章(原理篇) 微前端技术之模块联邦与动态加载
第五章(原理篇) 微前端技术之模块联邦与动态加载
|
10天前
|
前端开发 持续交付 开发工具
详细介绍Git的基本原理、在前端开发中的应用以及如何使用Git来优化团队协作
【6月更文挑战第14天】Git是前端开发中的必备工具,它通过分布式版本控制管理代码历史,支持分支、合并和冲突解决,促进团队协作。在前端开发中,Git用于代码追踪、版本控制、代码审查和持续集成部署,优化团队协作。制定分支策略、编写清晰提交信息、定期合并清理分支以及使用Git钩子和自动化工具能进一步提升效率。理解并善用Git,能有效提升前端项目的质量和开发效率。
25 3
|
12天前
|
监控 前端开发 Java
JVM系列9-后端编译与优化
JVM系列9-后端编译与优化
11 1
|
18天前
|
前端开发
前端React篇之React setState 调用的原理、React setState 调用之后发生了什么?是同步还是异步?
前端React篇之React setState 调用的原理、React setState 调用之后发生了什么?是同步还是异步?
|
12天前
|
缓存 自然语言处理 前端开发
JVM系列8-前端编译与优化
JVM系列8-前端编译与优化
9 0
|
1月前
|
前端开发 JavaScript 虚拟化
第四章(原理篇) 前端容器技术
第四章(原理篇) 前端容器技术
|
1月前
|
Java 数据库连接 Spring
K8S+Docker理论与实践深度集成java面试jvm原理
K8S+Docker理论与实践深度集成java面试jvm原理
|
1月前
|
JavaScript 前端开发
深入了解前端框架Vue.js的响应式原理
本文将深入探讨Vue.js前端框架的核心特性之一——响应式原理。通过分析Vue.js中的数据绑定、依赖追踪和虚拟DOM等机制,读者将对Vue.js的响应式系统有更深入的理解,从而能够更好地利用Vue.js构建灵活、高效的前端应用。