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

文章目录

  • 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.mfbz.cn/a/27553.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

了解ASEMI代理英飞凌TLE6208-6G其功能和应用的综合指南

编辑-Z TLE6208-6G是一款高度集成、通用且高效的汽车半桥驱动器&#xff0c;由英飞凌设计。这种功能强大的设备专门设计用于满足汽车应用的苛刻要求&#xff0c;如控制直流电机、螺线管和电阻负载。在本文中&#xff0c;我们将深入研究TLE6208-6G的功能、优点和应用&#xff0…

实现表白墙

我们已经学习了Http以及Servlet类的相关知识 今天我们来实操一下,实现一个简单的既有前端又有后端的网站–表白墙 之前在学习前端的时候已经写过了表白墙的前端代码,存在两个问题 1.页面重启,数据丢失 2.数据只是在本地的,别人看不见 那么这样的问题我们要咋样解决呢? 引入…

(七)CSharp-CSharp图解教程版-事件

一、发布者和订阅者 发布者/订阅者模式&#xff08;publish/subscriber pattern&#xff09;&#xff1a; 很多程序都有一个共同的需求&#xff0c;即当一个特定的程序事件发生时&#xff0c;程序的其他部分可以得到该事件已经发生的通知。 发布者&#xff1a; 发布者类定义…

Excel函数VLOOKUP常用方法

一、基础用法 1、精确匹配 公式&#xff1a;VLOOKUP(待匹配值&#xff0c;查找范围&#xff0c;范围列数&#xff0c;查找方式) 定义好要输出表的表头和第一列&#xff0c;第一列即为要查找和匹配的父内容&#xff0c;在第二列输入公式&#xff0c;被查找表中一定也要将待查…

基于SPAD / SiPM技术的激光雷达方案

激光雷达(LiDAR)是一种测距技术&#xff0c;近年来越来越多地用于汽车先进驾驶辅助系统(ADAS)、手势识别和3D映射等应用。尤其在汽车领域&#xff0c;随着传感器融合的趋势&#xff0c;LiDAR结合成像、超声波、毫米波雷达&#xff0c;互为补足&#xff0c;为汽车提供全方位感知…

【力扣刷题 | 第六天】

目录 前言&#xff1a; 344. 反转字符串 - 力扣&#xff08;LeetCode&#xff09; 541. 反转字符串 II - 力扣&#xff08;LeetCode&#xff09; 今天我们进入字符串章节的刷题旅程&#xff0c;希望各位小伙伴可以和我一起坚持下去&#xff0c;一起征服力扣&#xff01; 前言…

前端前端学习不断

卷吧卷吧...&#xff0c;这东西什么时候是个头啊……

半导体器件基础(期末模电速成)

目录 1、半导体分类 2、PN结 3、二极管 4、稳压二极管 5、三极管 6、场效应管 1、半导体分类 2、PN结 3、二极管 伏安特性&#xff1a; 我们第七版模电书上给的正向导通压降分别约为0.7和0.2V&#xff0c;且硅的单向导电性更好 如何确定二极管状态&#xff1f; 阳极电压…

怎么快速掌握Python爬虫技术?

Python总的来说是一门比较容易入门的编程语言&#xff0c;因为它的语法简洁易懂&#xff0c;而且有很多优秀的教程和资源可供学习。相比其他编程语言&#xff0c;Python 的学习曲线较为平缓&#xff0c;初学者可以很快上手&#xff0c;但要想深入掌握 Python&#xff0c;还需要…

6款AI绘画生成器,让你的创作更有灵感

人工智能绘画听起来很高深&#xff0c;其原理是通过集成文本、图片和其他大数据数据来生成信息库&#xff0c;在输入文本描述的要求后&#xff0c;可以找到相应的视觉元素&#xff0c;然后拼凑起来生成符合文本描述的图片。 本文介绍非常好用的6款AI绘画生成工具 1.即时 AI 绘…

location.href 和 document.URL 与 document.documentURI

location.href 和 document.URL 与 document.documentURI 相同点 获取到的值相同 不同点 location.hrefurl可以赋值, 效果类似location.assign(url) , 可以后退 document.URL 与 document.documentURI 是只读的, 赋值无效 location.href locationwindow.location true lo…

HTTP编码杂谈

一 HTTP编码杂谈 ① 知识铺垫 1) 编码的英文叫encode --> 常见HTTP URL编码、Base64编码等目的&#xff1a; 转变为二进制的stream(字节流),便于网络传输备注&#xff1a; 一般都是基于utf-8编码2) 解码叫decode3) 乱码的根源&#xff1a; 编码和解码的方式不一致4) url…

Flask开发简易网站疑难点梳理

文章目录 整体总结创建项目独立的python环境windows下python独立环境目录结构linux下python独立环境目录结构 大概需要安装的第三方库使用websockt实现python代码与html界面的通讯界面F12中看到提示连接成功后立马连接关闭。 linux下数据库查询异常初次登录web的时候背景图片和…

智能指针(2)

智能指针&#xff08;2&#xff09; shared_ptr(共享型智能指针)基础知识特点引用计数器共享型智能指针结构理解 shared_ptr仿写删除器类计数器类shared_ptr类使用以及仿写代码的理解 循环引用_Weaks 初始化智能指针的方法 shared_ptr(共享型智能指针) 基础知识 在java中有一…

Hive | 报错锦集

知识目录 一、写在前面✨二、Hive启动hiveserver2报错&#x1f525;三、HiveServer2启动方式✨四、Hive执行SQL语句报一大堆日志&#x1f349;五、Hive使用Load加载数据报错&#x1f36d;六、Hive执行含Count的SQL语句报错&#x1f349;七、Hive执行SQL语句报/bin/java&#x1…

openGauss5.0之学习环境 Docker安装

文章目录 0.前言1. 准备软硬件安装环境1.1 软硬件环境要求1.2 修改操作系统配置1.2.1 关闭操作系统防火墙 1.3 设置字符集参数1.4 设置时区和时间&#xff08;可选&#xff09;关闭swap交换内存1.5 关闭RemoveIPC1.6 关闭HISTORY记录 2. 容器安装2. 1支持的架构和操作系统版本2…

ChatGPT+小红书的8种高级玩法

掌握了这套万能命令&#xff0c;让你快速做出小红书爆款文案! 一、用ChatGPT做定位 我是一个大龄的普通人&#xff0c;没有什么特殊的技能&#xff0c;接下来&#xff0c;请你作为一位小红书的账号定位专家&#xff0c;通过与我对话的方式&#xff0c;为我找到我的小红书账号定…

记录一个Invalid bound statement (not found)问题

SpringBootMyBatisPlus项目&#xff0c;非常简单&#xff0c;没有任何业务逻辑&#xff1a; 1. pom文件 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.…

Java8 Stream详解及结束操作方法使用示例(三)

结束操作是指结束 Stream 该如何处理的操作&#xff0c;并且会触发 Stream 的执行。下面是一些常用的结束操作方法。结束操作会对数据源进行遍历&#xff0c;因此是及早求值的。 Java8 Stream详解及中间操作方法使用示例&#xff08;一&#xff09; ​​​​​​​Java8 Strea…

java生成、识别条形码和二维码

一、概述 使用 zxing 开源库 Zxing主要是Google出品的&#xff0c;用于识别一维码和二维码的第三方库主要类:BitMatrix 位图矩阵MultiFormatWriter 位图编写器MatrixToImageWriter 写入图片 可以生成、识别条形码和二维码 内置三种尺寸&#xff1a;enum Size {SMALL, MIDDLE, …