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

news2025/6/21 11:59:25

文章目录

  • 1.前置了解的知识
    • 1.1 栈这种数据结构
    • 1.2 局部变量表和操作数栈
    • 1.3 三个字节码指令
  • 2.单独使用后置++与前置++
    • 2.1 后置++字节码指令
    • 2.2 前置++字节码指令
    • 2.3 总结
  • 3.需要返回值的情况下使用后置++与前置++
    • 3.1 后置++字节码指令
    • 3.2 前置++字节码指令
    • 3.3 总结
    • 3.4 练习
      • 🍀 练习一
      • 🍀 练习二
  • 4.⭐ 经典面试题
    • 4.1 后置++
    • 4.2 前置++

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

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

1.前置了解的知识

1.1 栈这种数据结构

  • 栈是一个**先入后出(FILO-First In Last Out)**的有序列表。

  • 栈(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的 一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)。

  • 根据栈的定义可知,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元 素最先删除,最先放入的元素最后删除。

栈数据结构.png

1.2 局部变量表和操作数栈

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

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

解读class字节码指令.png

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/631608.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

npm i安装依赖包报错proxy‘ config is set properly. See: ‘npm help config‘

npm i 报错proxy‘ config is set properly. See: ‘npm help config‘ 网上搜了解决方法&#xff1a; https://blog.csdn.net/zz00008888/article/details/127852233 但是执行完还是报错&#xff0c;查代理已经是false了 看到是cnpm镜像&#xff0c;于是用cnpm i 就运行成功…

设计模式篇---原型模式

文章目录 概念Java中的克隆方法实例使用场景 概念 定义&#xff1a;使用原型实例指定待创建对象的类型&#xff0c;并通过复制这个原型来创建新的对象。 原型模式主要有以下几部分组成&#xff1a; Prototype(抽象原型类):具体原型类的接口或者抽象类。 ConcretePrototype(具体…

Mybatis 全系列目录引导(持续更新)

基础篇 001Mybatis常用的网站及工具_存在,及合理的博客-CSDN博客GITHUB。https://blog.csdn.net/qq_26594041/article/details/131098123002Mybatis初始化引入_存在,及合理的博客-CSDN博客自动检测工程中的DataSource创建并注册SqlSessionFactory实例创建并注册SqlSessionTemp…

OpenGL光照之颜色

文章目录 创建一个光照场景 现实世界中有无数种颜色&#xff0c;每一个物体都有它们自己的颜色。我们需要使用&#xff08;有限的&#xff09;数值来模拟真实世界中&#xff08;无限&#xff09;的颜色&#xff0c;所以并不是所有现实世界中的颜色都可以用数值来表示的。然而我…

物联网Lora模块从入门到精通(四)对某些端口的初始化

一、前言 由于程序设计开发具有的不确定性&#xff0c;我们常常需要初始化某些特定的引脚&#xff0c;并读取引脚电平状态或向引脚输出高低电平。 二、代码实现 快速找到端口的初始化语句&#xff1a; 首先&#xff0c;找到board.c文件&#xff0c;在下图的位置&#xff0c;我…

【算法系列专栏介绍】

序言 你只管努力&#xff0c;其他交给时间&#xff0c;时间会证明一切。 文章标记颜色说明&#xff1a; 黄色&#xff1a;重要标题红色&#xff1a;用来标记结论绿色&#xff1a;用来标记一级论点蓝色&#xff1a;用来标记二级论点 决定开一个算法专栏&#xff0c;希望能帮助大…

什么是域控服务器?域控服务器功能?部署域控需要考虑因素?域控组策略功能?

一、什么是域控制服务器&#xff1f; 域控制器&#xff08;Domain Controller&#xff09;是在Windows Server操作系统上运行的一个服务角色&#xff0c;它用于管理和控制一个或多个计算机的安全策略、用户身份验证和授权等任务。域控制器通常是用于企业网络中的主要身份验证和…

性能测试从0到1实战,超详细性能测试计划编写汇总...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 1、测试背景 首先…

yt-dlp 使用教程

参考&#xff1a;yt-dlp 使用教程 下载yt-dlp.exe&#xff0c;地址&#xff1a;Releases yt-dlp/yt-dlp GitHub windows下载.exe版本&#xff0c;放到指定路径下&#xff0c;我的是C:\Users\bellychang\Downloads 查看视频所有分辨率 yt-dlp.exe --proxy socks5://127.0.0.…

github action 基于个人项目实践

前言: DevOps 和 Jenkins 作为一名开发&#xff0c;虽然也没有经常听到 Devops &#xff08;研发和运维一体化&#xff09;这个概念&#xff0c;但日常工作中已经无处不在地用着 DevOps 工具。自研也好&#xff0c;基于开源项目改造也好&#xff0c;互联网公司基本都会有自已的…

强化学习Q-learning实践

1. 引言 前篇文章介绍了强化学习系统红的基本概念和重要组成部分&#xff0c;并解释了Q-learning算法相关的理论知识。本文的目标是在Python3中实现该算法&#xff0c;并将其应用于实际的实验中。 闲话少说&#xff0c;我们直接开始吧&#xff01; 2. Taxi-v3 Env 为了使本文…

一文讲完Java常用设计模式(23种)

介绍 设计模式的起源可以追溯到20世纪80年代&#xff0c;当时面向对象编程开始流行。在这个时期&#xff0c;一些软件开发者开始注意到他们在不同的项目中遇到了相同的问题&#xff0c;并且他们开始寻找可重用的解决方案。这些解决方案被称为设计模式。最早提出设计模式的人是…

centos7的docker安装与简单介绍

docker的基本组成&#xff08;三要素&#xff09; 镜像容器仓库 理解&#xff1a;镜像可以理解成一个类&#xff0c;容器就是用这个类new出来的对象&#xff0c;仓库就是放镜像文件的。docker本身是容器运行载体或管理引擎 安装 安装gcc yum -y install gcc安装需要的软件…

Vcpkg介绍及使用

Vcpkg用于在Windows、Linux、Mac上管理C和C库&#xff0c;极大简化了第三方库的安装&#xff0c;它由微软开源&#xff0c;源码地址&#xff1a;https://github.com/Microsoft/vcpkg&#xff0c;最新发布版本为2023.04.15 Release&#xff0c;它的license为MIT。 在windows上安…

[解决方案]springboot怎么接受encode后的参数(参数通过=拼接)

springboot怎么接受encode后的参数(拼接& springboot怎么接受encode后的参数(拼接&)问题出现原因发送encode后的值在postman里面的情况这个时候该如何接受呢&#xff08;encode后的值接受&#xff09;controller层的代码用到的工具类CRequest springboot怎么接受encode…

软考A计划-系统架构师-官方考试指定教程-(14/15)

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例 &#x1f449;关于作者 专注于Android/Unity和各种游戏开发技巧&#xff0c;以及各种资源分享&am…

数组删除元素使用remove最优的方法

Array.prototype.remove function(from, to) { var rest this.slice((to || from) 1 || this.length); this.length from < 0 ? this.length from : from; return this.push.apply(this, rest); };

Anaconda安装及入门教程(Windows、Ubuntu)

文章目录 安装Anaconda3UbuntuWindows 使用换源设置不自动启用conda环境显示环境创建环境激活环境查找某个包的版本安装某个版本的包 虚拟环境中安装包删除虚拟环境删除特定的包复制环境设置代理UbuntuWindows 使用 conda-pack 离线导入、导出环境安装conda-pack导出导入 安装A…

简单使用nacos、openFeign和Sentinel(建议看源码和截图一起看)

1、Nacos 1、下载nacos&#xff0c;可以从结尾获取压缩包和源码 2、下方图例是两个服务程序注册成功到注册中心&#xff0c;并且配置从配置中心拉取&#xff0c;成功访问数据库 3、nacos中配置项里的内容 spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driv…

在linux上做移动开发必须知道这五个

导读随着越来越多的人依靠手机进行各种业务&#xff0c;移动应用开发的重要性也在不断增加。虽然他们与桌面应用程序有很多相似之处&#xff0c;但移动应用程序本身也具有一系列挑战和特殊性。因此&#xff0c;希望在当前市场找到有利就业的程序员将需要利用和发展当前需求的技…