一文打通:从字节码指令的角度解读前置后置自增自减(加加++减减--)

简介: 一文打通:从字节码指令的角度解读前置后置自增自减(加加++减减--)


javac进行编辑源文件,生成 class 字节码二进制文件。解读 class 字节码文件当中的字节码指令,可以帮助我们更好理解程序执行过程的机理。

关于前置加加、后置加加,我们通常记得的是先加1再操作、先操作再加1。在本文中,我们将以 Java底层真正执行的字节码指令角度更好理解为什么是这样,在一些比较复杂的判断执行先后顺序的时候使用字节码指令进行判断会更加的简单!

1.前置了解的知识

1.1 栈这种数据结构

  • 栈是一个**先入后出(FILO-First In Last Out)**的有序列表
  • 栈(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的 一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)。
  • 根据栈的定义可知,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元 素最先删除,最先放入的元素最后删除。

1.2 局部变量表和操作数栈

每个方法在被调用时都会分配一个独立的空间,该空间中又包括 局部变量表操作数栈 两个部分。

  • 局部变量表 用来存储方法中定义的局部变量、方法参数等等,它是在编译时确定大小的,具体的大小可以在字节码中看到。
  • 操作数栈 用来存储方法执行中的操作数据,操作数栈是一个后进先出(LIFO)的数据结构,Java 虚拟机在执行指令时会将数据压入操作数栈中,然后再从栈中取出数据进行计算。

1.3 三个字节码指令

public class ReadClass{
    public static void main(String[] args){
        int i = 10;
    }
}

编译生成:ReadClass.class

如何查看字节码?javap -c ReadClass.class,以上程序字节码如下:

public class ReadClass {
  public ReadClass();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
  public static void main(java.lang.String[]);
    // 主要分析下面的指令
    Code:
       0: bipush        10
       2: istore_1
       3: return
}

重点研究 main 方法中的字节码含义:

  1. bipush 10 指令:将字面量 10 压入操作数栈。
  2. istore_1 指令:将操作数栈中顶部数据弹出,然后将该数据存放到局部变量表的第1个位置(第0个位置存储的方法的参数args)。
  3. return 指令:方法结束。
public class ReadClass{
    public static void main(String[] args){
        int i = 10;
        int j = i;
    }
}

编译生成:ReadClass.class,再 javap -c ReadClass.class,以上程序字节码如下:

public class ReadClass {
  public ReadClass();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
  public static void main(java.lang.String[]);
    Code:
       // 将 字面量 10 压入操作数栈
       0: bipush        10
       // 将操作数栈顶的数据 10 弹出,将该数据放入局部变量表第 1 个位置,即完成了将 10 赋值给 i 的操作
       2: istore_1
       // 将局部变量表中第 1 个位置存储的数据复制一份,放到操作数栈当中。
       3: iload_1
       // 将操作数栈顶的数据 10 弹出,将该数据放入局部变量表第 2 个位置,即完成了将 10 赋值给 j 的操作
       4: istore_2
       // 方法结束
       5: return
}
  • iload_1 指令:将局部变量表中第1个位置存储的数据复制一份,放到操作数栈当中。
  • istore_2 指令:将操作数栈顶部数据弹出,将其存放到局部变量表的第2个位置上。

2.单独使用后置++与前置++

由于 ++ 与 – 原理相同,这里就以 ++ 为例进行演示。

2.1 后置++字节码指令

public class ReadClass{
    public static void main(String[] args){
        int i = 10;
        i++;
    }
}

编译生成:ReadClass.class,再 javap -c ReadClass.class,以上程序字节码如下:

public class ReadClass {
  public ReadClass();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
  public static void main(java.lang.String[]);
    Code:
       // 将 字面量 10 压入操作数栈
       0: bipush        10
       // 将操作数栈顶的数据 10 弹出,将该数据放入局部变量表第 1 个位置,即完成了将 10 赋值给 i 的操作
       2: istore_1
       // 将局部变量表第 1 个位置的数据加 1,即从 10 变成了 11
       3: iinc          1, 1
       // 方法结束
       6: return
}
  • iinc 1, 1 指令:将局部变量表中第1个位置数据加1

2.2 前置++字节码指令

public class ReadClass{
    public static void main(String[] args){
        int i = 10;
        ++i;
    }
}

编译生成:ReadClass.class,再 javap -c ReadClass.class,以上程序字节码如下:

public class ReadClass {
  public ReadClass();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
  public static void main(java.lang.String[]);
    Code:
       // 将 字面量 10 压入操作数栈
       0: bipush        10
       // 将操作数栈顶的数据 10 弹出,将该数据放入局部变量表第 1 个位置,即完成了将 10 赋值给 i 的操作
       2: istore_1
       // 将局部变量表第 1 个位置的数据加 1,即从 10 变成了 11
       3: iinc          1, 1
       // 方法结束
       6: return
}
  • iinc 1, 1 指令:将局部变量表中第1个位置数据加1

2.3 总结

分析了单独使用前置++和后置++的指令,我们发现字节码指令是一样的,实际上都是将局部变量表对应位置的数据进行加1操作。

🚩 当单独使用++-- 时,不需要关心其返回值,因此前置和后置的效率是一样的。实际上,在编译时,编译器可能会将单独使用++-- 运算符优化为一条简单的指令 iinc,因此在机器指令级别上,它们的执行效率是相同的。

3.需要返回值的情况下使用后置++与前置++

3.1 后置++字节码指令

public class ArithmeticOperator {
    public static void main(String[] args) {
        /*
          后置 ++ 字节码指令:
            public class ArithmeticOperator {
              public ArithmeticOperator();
                Code:
                   0: aload_0
                   1: invokespecial #1                  // Method java/lang/Object."<init>":()V
                   4: return
              public static void main(java.lang.String[]);
                Code:
                   0: bipush        10
                   2: istore_1
                   3: iload_1
                   4: iinc          1, 1
                   7: istore_2
                   8: return
            }
         */
        /*
            0: bipush 10:将数据 10 放到操作数栈中
            2: istore_1:将操作数栈顶数据 10 弹出赋值给变量i,即存到局部变量表第1个位置
         */
        int i = 10;
        /*
            3: iload_1 将局部变量表第1个位置的数据 10 复制一份放入到操作数栈
            4: iinc 1, 1 将局部变量表第1个位置的数据 10 自加1 变为 11
            7: istore_2 将操作数栈顶数据 10 弹出赋值给变量k,即存到局部变量表第2个位置
         */
        int k = i++;
    }
}

我们可以看到在 int k = i++; 这条语句中,实际执行了三个字节码指令:

  1. iload_1:将局部变量表第1个位置的数据 10 复制一份放入到操作数栈。
  2. iinc 1, 1:将局部变量表第1个位置的数据 10 自加1 变为 11。
  3. istore_2:将操作数栈顶数据 10 弹出赋值给变量k,即存到局部变量表第2个位置。

因此,我们在谈到 后置++ 时,通常说是 先操作再加1 ,那实际上:

  • 这个“先操作”从字节码指令的角度看就是先将局部变量表的对应数据复制一份压入操作数栈;
  • “再加1”就是再将局部变量对应数据加1,而操作数栈中保存的数据还是原本数据。
  • 紧接着的 对k的赋值 操作实际是 从操作数栈顶弹出原本数据存储到局部变量表即赋值给k。

3.2 前置++字节码指令

public class ArithmeticOperator {
    public static void main(String[] args) {
        /*
            public class ArithmeticOperator {
              public ArithmeticOperator();
                Code:
                   0: aload_0
                   1: invokespecial #1                  // Method java/lang/Object."<init>":()V
                   4: return
              public static void main(java.lang.String[]);
                Code:
                   0: bipush        10
                   2: istore_1
                   3: iinc          1, 1
                   6: iload_1
                   7: istore_2
                   8: return
            }
         */
        /*
            0: bipush 10:将数据 10 放到操作数栈中
            2: istore_1:将操作数栈顶数据 10 弹出赋值给变量i,即存到局部变量表第1个位置
         */
        int i = 10;
        /*
            3: iinc 1, 1 将局部变量表第1个位置的数据 10 自加1 变为 11
            6: iload_1 将局部变量表第1个位置的数据 11 复制一份放入到操作数栈
            7: istore_2 将操作数栈顶数据 11 弹出赋值给变量k,即存到局部变量表第2个位置
         */
        int k = ++i;
    }
}

3.3 总结

在需要返回值的情况下我们比较发现:

  • 后置++(先操作再加1):先复制一份局部变量表对应数据压入到操作数栈,再将局部变量表对应数据加1。
  • 前置++(先加1再操作):先将局部变量表对应数据加1,再复制一份局部变量表对应数据压入到操作数栈。

这里就可以看出压入到操作数栈的数据是不同的,那么最后弹出 操作数栈顶的该数据 作为 返回值 进行 赋值操作的结果也是不同的。

3.4 练习

🍀 练习一

int a = 5;
int b = a++; // 先复制一份数据压入操作数栈,再将局部变量表数据+1,最后从栈中弹出数据作为返回值赋值给 b
System.out.println("b = " + b); // 5
b = a++;
System.out.println("a = " + a); // 7
System.out.println("b = " + b); // 6
int c = 10;
int d = --c; // 先将数据-1,再复制一份压入操作数栈,再从栈中弹出该数据作为返回值赋值给 d
System.out.println("c = " + c); // 9
System.out.println("d = " + d); // 9

🍀 练习二

int i = 10;
/*
  等式右边从左向右执行:
  - 先 i++:复制一份数据 10 到操作数栈,将局部变量表数据+1变为11,弹出操作数栈中数据 10 作为返回值,即 i++ 的返回值是 10,i变成了 11
  - 再 ++i:将局部变量表数据 11 加1变为 12,复制一份数据 12 到操作数栈,弹出栈中数据 12 作为返回值,即 ++i 的返回值是 12,i变成了 12
  - 最后 10 + 12 得到 22 赋值给 k
*/
int k = i++ + ++i; 
System.out.println(k); // 22
int f = 10;
/*
  等式右边从左向右执行:
  - 先 f++:复制一份数据 10 到操作数栈,将局部变量表数据+1变为11,弹出操作数栈中数据 10 作为返回值,即 f++ 的返回值是 10,f变成了 11
  - 即 ( f++ + f ) 变为了 ( 10 + f ):此时 f 的值变成了 11 ,因此 将 (10 + 11) 的结果赋值给 m
  - 最后 10 + 12 得到 22 赋值给 k
*/
int m = f++ +f;
System.out.println(m); // 21
System.out.println(f); // 11

4.⭐ 经典面试题

4.1 后置++

/*
    0: bipush 10:将数据 10 放到操作数栈中
    2: istore_1:将操作数栈顶数据 10 弹出赋值给变量i,即存到局部变量表第1个位置
 */
int i = 10;
/*
    3: iload_1 将局部变量表第1个位置的数据 10 复制一份放入到操作数栈
    4: iinc 1, 1 将局部变量表第1个位置的数据 10 自加1 变为 11
    7: istore_1 将操作数栈顶数据 10 弹出放到局部变量表第1个位置即将 10 赋值给变量i
 */
i = i++;
System.out.println(i); // 10

4.2 前置++

/*
    0: bipush 10:将数据 10 放到操作数栈中
    2: istore_1:将操作数栈顶数据 10 弹出赋值给变量i,即存到局部变量表第1个位置
 */
int i = 10;
/*
    3: iinc 1, 1 将局部变量表第1个位置的数据 10 自加1 变为 11 
    6: iload_1 将局部变量表第1个位置的数据 11 复制一份放入到操作数栈
    7: istore_1 将操作数栈顶数据 11 弹出放到局部变量表第1个位置即将 11 赋值给变量i
 */
i = ++i;
System.out.println(i); // 11


相关文章
|
运维 安全 Cloud Native
阿里云云安全中心不同版本的区别
阿里云云安全中心不同版本的区别,云安全中心基础版免费、防病毒班432元一年、高级版优惠价969元一年,还有企业版和旗舰版可选,阿里云百科分享阿里云安全中心详细介绍,包括云安全中心功能、不同版本价格表以及有必要购买说明
640 0
|
机器人 C++ Python
ROS2教程 02 功能包
本文是关于ROS2(机器人操作系统2)中功能包(package)管理的教程,介绍了如何检查功能包的依赖、创建新功能包、列出可执行文件、列出所有功能包、查询功能包的位置和描述信息,以及为C++和Python功能包配置必要的文件。
541 0
|
JavaScript 安全 编译器
TypeScript 与 Jest 测试框架的结合使用,从 TypeScript 的测试需求出发,介绍了 Jest 的特点及其与 TypeScript 结合的优势,详细讲解了基本测试步骤、常见测试场景及异步操作测试方法
本文深入探讨了 TypeScript 与 Jest 测试框架的结合使用,从 TypeScript 的测试需求出发,介绍了 Jest 的特点及其与 TypeScript 结合的优势,详细讲解了基本测试步骤、常见测试场景及异步操作测试方法,并通过实际案例展示了其在项目中的应用效果,旨在提升代码质量和开发效率。
282 6
|
开发工具 开发者
快速部署小游戏
该图示展示了一种快速部署小游戏的流程,包括:1) 使用阿里云开发工具;2) 选择小游戏模板;3) 进行代码编辑与资源管理;4) 实时预览与调试;5) 完成后进行版本提交;6) 通过云端打包服务;7) 发布到应用市场。整个过程高效便捷,旨在帮助开发者迅速创建并发布小游戏。
|
存储 编解码 UED
网站图片JPG、PNG、GIF哪个好,该选择谁
网站图片JPG、PNG、GIF哪个好,该选择谁
668 0
|
JSON 运维 安全
深入探索Linux的lsns命令:处理与分析Linux命名空间
`lsns`命令是Linux中用于查看命名空间信息的工具,帮助管理和隔离系统资源。它显示命名空间的状态、类型、进程和挂载点,适用于性能优化、故障排查。命令特点包括丰富的参数选项(如 `-t`、`-p`、`-n`),清晰的表格输出和JSON格式支持。示例:列出所有命名空间用`lsns`,列出网络命名空间用`lsns -t net`。使用时注意权限,结合其他工具,并考虑版本兼容性。
|
供应链 JavaScript 前端开发
使用Django和Vue实现电子商务网站的后端和前端
【4月更文挑战第10天】本文介绍了使用Django和Vue构建电子商务网站的后端与前端方法。Django作为Python的Web框架负责后端,其模型-视图-控制器设计简化了商品管理、购物车和订单处理。Vue.js用于前端,提供数据驱动和组件化的用户界面。通过定义Django模型和视图处理请求,结合Vue组件展示商品和管理购物车,开发者可构建交互性强的电商网站。虽然实际开发涉及更多细节,但本文为入门提供了基础指导。
428 2
|
安全 网络协议 网络安全
安全开发实战(2)---域名反查IP
本文介绍了域名与IP地址的关系以及域名反查IP的作用。通过DNS,域名与IP地址相互映射,方便用户访问网络资源。在渗透测试中,反查IP用于确定服务器真实地址、进行目标侦察和安全性评估,也能检测DNS劫持。文中提供了一些Python代码示例,演示了如何进行域名反查IP和批量处理,并强调在处理时要注意去除换行符以避免错误。
|
机器学习/深度学习 存储 文字识别
OCR技术原理
OCR技术通过识别图像中的字符转化为可编辑文本,涉及图像获取、预处理、字符分割、特征提取、字符识别和后处理等步骤。现代OCR利用机器学习和深度学习提升识别准确性,应对各种图像质量和文本类型挑战。随着技术进步,OCR广泛应用于文档扫描、数据录入和车牌识别等领域。
|
消息中间件 RocketMQ 微服务
微服务异步架构---MQ之RocketMQ(二)
“我们大家都知道把一个微服务架构变成一个异步架构只需要加一个MQ,现在市面上有很多MQ的开源框架。到底选择哪一个MQ的开源框架才合适呢?”
微服务异步架构---MQ之RocketMQ(二)