【JVM】字节码技术:分析语法糖原理(前端编译优化)
admin
2024-02-13 02:38:56
0

一、概念

所谓的 语法糖 ,其实就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成 和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利(给糖吃 嘛)

注意:以下代码的分析,借助了 javap 工具,idea 的反编译功能,idea 插件 jclasslib 等工具。另外, 编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了几乎等价的 java 源码方式,并不是编译器还会转换出中间的 java 源码,切记。

二、默认构造器

public class Candy1 {
}
public class org.example.classLoading.Candy1...
Constant pool:#1 = Methodref          #3.#13         // java/lang/Object."":()V...
{// 编译器优化:这个无参构造是编译器帮助我们加上的public org.example.classLoading.Candy1();...Code:stack=1, locals=1, args_size=10: aload_01: invokespecial #1	//super();4: returnLineNumberTable: ...LocalVariableTable:Start  Length  Slot  Name   Signature0       5     0  this   Lorg/example/classLoading/Candy1;
}

二、自动拆装箱

代码片段1 :显然当前版本太麻烦了,需要在基本类型和包装类型之间来回转换(尤其是集合类中操作的都是 包装类型)

public class Candy2 {public static void main(String[] args) {Integer x = Integer.valueOf(1);int y = x.intValue();}
}

因此这些转换的事情在 JDK 5 以后都由编译器在编译阶段完成。即 代码片段1 都会在编译阶段被转换为 代码片段2

代码片段2: 这段代码在 JDK 5 之前是无法编译通过的,JDK 5 开始自动优化

public class Candy2 {public static void main(String[] args) {Integer x = 1;	//编译器进行优化👆int y = x;	//编译器进行优化👆}
}

三、泛型擦除、反射泛型

泛型是在 JDK 5 开始加入的特性,主要是在编译期间做类型检查。
java 在编译泛型代码后会执行 泛型擦除 的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理:取值时,字节码需做一个强制类型转换

public class Candy3 {public static void main(String[] args) {List list=new ArrayList<>();list.add(10);Integer x=list.get(0);//取值时,字节码需做一个强制类型转换}
}

泛型是为了在编译期间的类型检查,泛型擦除是为了便于字节码处理

擦除的是字节码上的泛型信息,可以看到 LocalVariableTypeTable(局部变量类型表) 仍然保留了方法参数泛型的信息

public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=3, args_size=10: new           #23: dup4: invokespecial #37: astore_18: aload_19: bipush        1011: invokestatic  #4                  // 1.Integer.valueOf(10)14: invokeinterface #5,  2            // 2.注意为Object:List.add(Object)19: pop20: aload_121: iconst_022: invokeinterface #6,  2            // 3.list.get(Object);27: checkcast     #7                  // 4.注意强制类型转换:(Integer)list.get(0);30: astore_231: returnLineNumberTable: ...LocalVariableTable:Start  Length  Slot  Name   Signature0      32     0  args   [Ljava/lang/String;8      24     1  list   Ljava/util/List;31       1     2     x   Ljava/lang/Integer;LocalVariableTypeTable:Start  Length  Slot  Name   Signature8      24     1  list   Ljava/util/List;

反射泛型

注意:局部遍历的泛型信息无法通过反射获取。只有类结构上的泛型可以通过反射获取

public class Candy3 {public static void main(String[] args) throws NoSuchMethodException {Method test = Candy3.class.getMethod("test", List.class, Map.class);Type[] types = test.getGenericParameterTypes();for (Type type : types) {if (type instanceof ParameterizedType){ParameterizedType parameterizedType= (ParameterizedType) type;System.out.println("原始类型 - "+parameterizedType.getRawType());Type[] arguments = parameterizedType.getActualTypeArguments();for (int i=0;iSystem.out.printf("泛型参数[%d] - %s\n",i,arguments[i]);}}}}public Set test(List list,Map map){return new HashSet<>();}
}
原始类型 - interface java.util.List
泛型参数[0] - class java.lang.String
原始类型 - interface java.util.Map
泛型参数[0] - class java.lang.Integer
泛型参数[1] - class java.lang.Object

四、可变参数

可变参数也是 JDK 5 开始加入的新特性:

public class Candy4 {public static void main(String[] args) {foo("hello", "world");}public static void foo(String... args) {String[] array = args; //可以直接赋值System.out.println(array);}
}

可变参数 String… args 其实是一个 String[] args ,从代码中的赋值语句中就可以看出来。

同样 java 编译器会在编译期间将上述代码变换为:

注意:如果调用了 foo() 则等价代码为 foo(new String[]{}) ,创建了一个空的数组,而不会传递 null 进去

public class Candy4 {public static void main(String[] args) {foo(new String[]{"hello", "world"});	//编译器进行优化}public static void foo(String[] args) {String[] array = args; // 可以直接赋值System.out.println(array);}
}

五、foreach循环

仍是 JDK 5 开始引入的语法糖。

注意: foreach 循环写法,能够配合数组,以及所有实现了 Iterable 接口的集合类一起使用。其中 Iterable 用来获取集合的迭代器( Iterator )

数组的循环

public class Candy5_1 {public static void main(String[] args) {int[] array = {1, 2, 3, 4, 5};for (int e : array) {System.out.println(e);}}
}

编译器编译后的字节码:

public class Candy5_1 {public static void main(String[] args) {int[] array = new int[]{1, 2, 3, 4, 5};	//编译器进行优化for(int i = 0; i < array.length; ++i) {	//编译器进行优化int e = array[i];System.out.println(e);}}
}

集合的循环

public class Candy5_2 {public static void main(String[] args) {List list = Arrays.asList(1,2,3,4,5);for (Integer i : list) {System.out.println(i);}}
}

编译器编译后的字节码:迭代器的调用

public class Candy5_2 {public static void main(String[] args) {List list = Arrays.asList(1, 2, 3, 4, 5);Iterator iter = list.iterator();	//编译器进行优化while(iter.hasNext()) {	//编译器进行优化Integer e = (Integer)iter.next();System.out.println(e);}}
}

六、switch 字符串

从 JDK 7 开始,switch 可以作用于字符串和枚举类,这个功能其实也是语法糖,例如:

注意:配合 String 和枚举使用时,不能 case “null”。因为 null 不能调用hashCode()

public class Candy6_1 {public static void choose(String str) {switch (str) {case "hello": {System.out.println("h");break;}case "world": {System.out.println("w");break;}}}
}

编译器编译后的字节码:将字符串转换为 byte 类型,再进行比较

public class Candy6_1 {public static void choose(String str) {byte x = -1;switch(str.hashCode()) {case 99162322: //"hello".(hashCode):编译器进行优化if (str.equals("hello")) {x = 0;}break;case 113318802: //"world".(hashCode):编译器进行优化if (str.equals("world")) {x = 1;}}switch(x) {case 0:System.out.println("h");break;case 1:System.out.println("w");}}
}

七、switch 枚举

switch 枚举的例子,原始代码:

public class Candy7 {public static void foo(Sex sex) {switch (sex) {case MALE:System.out.println("男"); break;case FEMALE:System.out.println("女"); break;}}
}
enum Sex {MALE, FEMALE
}

编译器编译后的字节码:

public class Candy7 {//编译器进行优化:定义一个合成类,仅 jvm 使用static class $MAP {static int[] map = new int[2]; //大小即为枚举元素个数static {map[Sex.MALE.ordinal()] = 1;	//存储case对应的数字map[Sex.FEMALE.ordinal()] = 2;}}public static void foo(Sex sex) {switch (sex.ordinal()) {	//编译器进行优化case 1:System.out.println("男");break;case 2:System.out.println("女");break;}}
}

八、枚举类

JDK 7 新增了枚举类,以前面的性别枚举为例:

enum Sex {MALE, FEMALE
}

编译器编译后的字节码:

final class Sex extends Enum{	//编译器进行优化static{MALE = new Sex("MALE", 0);FEMALE = new Sex("FEMALE", 1);$VALUES = new Sex[]{MALE, FEMALE};}public static final Sex MALE;public static final Sex FEMALE;private static final Sex[] $VALUES;private Sex(String name, int ordinal) {super(name, ordinal);}public static Sex[] values(){return (Sex[])$VALUES.clone();}public static Sex valueOf(String name){return (Sex)Enum.valueOf(Sex.class,name);}
}

九、try-with-resources

JDK 7 开始新增了对需要关闭的资源处理的特殊语法 try-with-resources

其中资源对象需要实现 AutoCloseable 接口,例如 InputStream 、 OutputStream 、 Connection 、 Statement 、 ResultSet 等接口都实现了 AutoCloseable ,使用 try-withresources 可以不用写 finally 语句块,编译器会帮助生成关闭资源代码,例如:

public class Candy9 {public static void main(String[] args) {try(资源变量 = 创建资源对象){} catch( ) {}}
}

编译器编译后的字节码:

public class Candy9 {public static void main(String[] args) {try {	//编译器进行优化InputStream is = new FileInputStream("d:\\1.txt");Throwable t = null;try {System.out.println(is);} catch (Throwable e1) {t = e1;throw e1;} finally {if (is != null) {if (t != null) {try {is.close();} catch (Throwable e2) {t.addSuppressed(e2);	//注意:输出外层套内存的异常}} else {is.close();}}}} catch (IOException e) {e.printStackTrace();}}
}

十、方法重写时的桥接方法

我们都知道,方法重写时对返回值分两种情况:

  • 父子类的返回值完全一致
  • 子类返回值可以是父类返回值的子类(比较绕口,见下面的例子)
class A {public Number m() {return 1;}
}
class B extends A {@Overridepublic Integer m() {	//返回值类型与父类不一致,那怎么说是重写呢?return 2;}
}

对于子类,编译器编译后的字节码:

class B extends A {public Integer m() {return 2;}// 合成方法,仅JVM可见:真正的重写方法public synthetic bridge Number m() {return m();}
}

验证

public class Candy10 {public static void main(String[] args) {Method[] declaredMethods = B.class.getDeclaredMethods();for (Method declaredMethod : declaredMethods) {System.out.println(declaredMethod);	//输出两个m()}}
}
#两个m()
public java.lang.Integer org.example.classLoading.candy.B.m()
public java.lang.Number org.example.classLoading.candy.B.m()

十一、匿名内部类

源代码:

public class Candy11 {public static void main(String[] args) {Runnable runnable = new Runnable() {@Overridepublic void run() {System.out.println("ok");}};}
}

编译器编译后的字节码:

public class Candy11 {public static void main(String[] args) {Runnable runnable = new Candy11$1();}
}
// 额外生成的类
final class Candy11$1 implements Runnable {Candy11$1() {}public void run() {System.out.println("ok");}
}

引用 局部变量 的匿名内部类,源代码:

注意

为什么匿名内部类引用局部变量时,局部变量必须是 final 的?

  • 因为在创建 Candy11$1 对象时,将 x 的值赋值给了 Candy111对象的val1 对象的 val1对象的valx 属性,所以 x 不应该再发生变 化了。如果变化,那么 val$x 属性没有机会再跟着一起变化
public class Candy11 {public static void test(final int x) {Runnable runnable = new Runnable() {@Overridepublic void run() {System.out.println("ok:" + x);}};}
}

编译器编译后的字节码:

public class Candy11 {public static void test(final int x) {Runnable runnable = new Candy11$1(x);}
}
// 额外生成的类
final class Candy11$1 implements Runnable {int val$x;Candy11$1(int x) {this.val$x = x;}public void run() {System.out.println("ok:" + this.val$x);}
}

相关内容

热门资讯

linux入门---制作进度条 了解缓冲区 我们首先来看看下面的操作: 我们首先创建了一个文件并在这个文件里面添加了...
C++ 机房预约系统(六):学... 8、 学生模块 8.1 学生子菜单、登录和注销 实现步骤: 在Student.cpp的...
A.机器学习入门算法(三):基... 机器学习算法(三):K近邻(k-nearest neigh...
数字温湿度传感器DHT11模块... 模块实例https://blog.csdn.net/qq_38393591/article/deta...
有限元三角形单元的等效节点力 文章目录前言一、重新复习一下有限元三角形单元的理论1、三角形单元的形函数(Nÿ...
Redis 所有支持的数据结构... Redis 是一种开源的基于键值对存储的 NoSQL 数据库,支持多种数据结构。以下是...
win下pytorch安装—c... 安装目录一、cuda安装1.1、cuda版本选择1.2、下载安装二、cudnn安装三、pytorch...
MySQL基础-多表查询 文章目录MySQL基础-多表查询一、案例及引入1、基础概念2、笛卡尔积的理解二、多表查询的分类1、等...
keil调试专题篇 调试的前提是需要连接调试器比如STLINK。 然后点击菜单或者快捷图标均可进入调试模式。 如果前面...
MATLAB | 全网最详细网... 一篇超超超长,超超超全面网络图绘制教程,本篇基本能讲清楚所有绘制要点&#...
IHome主页 - 让你的浏览... 随着互联网的发展,人们越来越离不开浏览器了。每天上班、学习、娱乐,浏览器...
TCP 协议 一、TCP 协议概念 TCP即传输控制协议(Transmission Control ...
营业执照的经营范围有哪些 营业执照的经营范围有哪些 经营范围是指企业可以从事的生产经营与服务项目,是进行公司注册...
C++ 可变体(variant... 一、可变体(variant) 基础用法 Union的问题: 无法知道当前使用的类型是什...
血压计语音芯片,电子医疗设备声... 语音电子血压计是带有语音提示功能的电子血压计,测量前至测量结果全程语音播报࿰...
MySQL OCP888题解0... 文章目录1、原题1.1、英文原题1.2、答案2、题目解析2.1、题干解析2.2、选项解析3、知识点3...
【2023-Pytorch-检... (肆十二想说的一些话)Yolo这个系列我们已经更新了大概一年的时间,现在基本的流程也走走通了,包含数...
实战项目:保险行业用户分类 这里写目录标题1、项目介绍1.1 行业背景1.2 数据介绍2、代码实现导入数据探索数据处理列标签名异...
记录--我在前端干工地(thr... 这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 前段时间接触了Th...
43 openEuler搭建A... 文章目录43 openEuler搭建Apache服务器-配置文件说明和管理模块43.1 配置文件说明...