热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

面试官:你说你懂i++跟++i的区别,你看下这段代码的运行结果吧

面试官:“说一说i跟i的区别”我:“i是先把i的值拿出来使用,然后再对i1,i是先对i1,然后再去使用i”

在这里插入图片描述

面试官:“说一说i++跟++i的区别”

我:“i++是先把i的值拿出来使用,然后再对i+1,++i是先对i+1,然后再去使用i”

面试官:“那你看看下面这段代码,运行结果是什么?”

public static void main(String[] args) {int j &#61; 0;for (int i &#61; 0; i <10; i&#43;&#43;) {j &#61; (j&#43;&#43;);}System.out.println(j);
}

“以我多年的开发经验来看&#xff0c;它必然不会是10”

面试官&#xff1a;

在这里插入图片描述

我&#xff1a;“哈哈…&#xff0c;开个玩笑&#xff0c;结果为0啦”

面试官&#xff1a;“你能说说为什么吗&#xff1f;”

我&#xff1a;“因为j&#43;&#43;这个表达式每次返回的都是0&#xff0c;所以最终结果就是0”

面试官&#xff1a;“小伙子不错&#xff0c;那你能从JVM的角度讲一讲为什么嘛&#xff1f;”

我心想&#xff1a;这货明显是在搞事情啊&#xff0c;这么快就到JVM了&#xff1f;还好我有准备。

首先我们知道&#xff0c;JVM的运行时数据区域是分为好几块的&#xff0c;具体分布如下图所示&#xff1a;

在这里插入图片描述

现在我们主要关注其中的虚拟机栈&#xff0c;关于虚拟机栈&#xff0c;我们知道它有以下几个特点&#xff1a;

Java虚拟机栈是线程私有的&#xff0c;它的生命周期和线程相同
Java虚拟机栈是由一个个栈帧组成&#xff0c;线程在执行一个方法时&#xff0c;便会向栈中放入一个栈帧。
每一个方法所对应的栈帧又包含了以下几个部分
局部变量表
操作数栈
方法出口

那么现在虚拟机栈就可以表示成下面这个样子&#xff1a;

在这里插入图片描述

其中的局部变量表存放了编译期可知的各种基本数据类型&#xff08;boolean、byte、char、short、int、float、long、double&#xff09;、对象引用。局部变量表所需的内存空间在编译期间完成分配&#xff0c;当进入一个方法时&#xff0c;这个方法需要在帧中分配多大的局部变量空间是完全确定的&#xff0c;在方法运行期间不会改变局部变量表的大小。

局部变量表的最小存储单元为Slot&#xff08;槽&#xff09;&#xff0c;其中64位长度的long和double类型的数据会占用2个Slot&#xff0c;其余的数据类型只占用1个。所以我们可以将局部变量表分为一个个的存储单元&#xff0c;每个存储单元有自己的下标位置&#xff0c;在对数据进行访问时可以直接通过下标来访问

操作数栈对于数据的存储跟局部变量表是一样的&#xff0c;但是跟局部变量表不同的是&#xff0c;操作数栈对于数据的访问不是通过下标而是通过标准的栈操作来进行的&#xff08;压入与弹出&#xff09;&#xff0c;之后在分析字节码指令时我们会很明显的感觉到这一点。另外还有&#xff0c;对于数据的计算是由CPU完成的&#xff0c;所以CPU在执行指令时每次会从操作数栈中弹出所需的操作数经过计算后再压入到操作数栈顶。

以执行下面这段代码为例&#xff1a;

public static void main(String[] args){int a &#61; 2;int b &#61; 3;int c &#61; a &#43; b;
}

这个过程如下所示

在这里插入图片描述

这两步完成了局部变量a的赋值&#xff0c;同理b的赋值也一样&#xff0c;a,b完成赋值后此时的状态如下图所示

在这里插入图片描述

此时要执行a&#43;b的运算了&#xff0c;所以首先要将需要的操作数加载到操作数栈&#xff0c;执行运算时再将操作数从栈中弹出&#xff0c;由CPU完成计算后再将结果压入到栈中&#xff0c;整个过程如下&#xff1a;

在这里插入图片描述
到这里还没有完哦&#xff0c;还剩最后一步&#xff0c;需要将计算后的结果赋值给c&#xff0c;也就是要将操作数栈的数据弹出并赋值给局部变量表中的第三个槽位

在这里插入图片描述

OK&#xff0c;到这一步整个过程就完成了

面试官&#xff1a;“嗯&#xff0c;说的不错&#xff0c;但是你还是没解释为什么最开始的那个问题&#xff0c;为什么j&#61;j&#43;&#43;的结果会是0呢&#xff1f;”

我&#xff1a;“面试官您好&#xff0c;要解释这个问题上面的知识都是基础&#xff0c;真正要说明白这个问题我们需要从字节码入手。”

我们进入到这段代码编译好的.class文件目录下执行&#xff1a;javap -c xxx.class,得到其字节码如下&#xff1a;

// 为方便阅读将对应代码也放到这里
public static void main(String[] args) {int j &#61; 0;for (int i &#61; 0; i <10; i&#43;&#43;) {j &#61; (j&#43;&#43;);}System.out.println(j);
}

public static void main(java.lang.String[]);Code:0: iconst_0 // 将常数0压入到操作数栈顶1: istore_1 // 将操作数栈顶元素弹出并压入到局部变量表中1号槽位&#xff0c;也就是j&#61;02: iconst_0 // 将常数0压入到操作数栈顶3: istore_2 // 将操作数栈顶元素弹出并压入到局部变量表中2号槽位&#xff0c;也就是i&#61;04: iload_2 // 将2号槽位的元素压入操作数栈顶5: bipush 10 // 将常数10压入到操作数栈顶&#xff0c;此时操作数栈中有两个数&#xff08;常数10&#xff0c;以及i&#xff09;7: if_icmpge 21 // 比较操作数栈中的两个数&#xff0c;如果i>&#61;10,跳转到第21行10: iload_1 // 将局部变量表中的1号槽位的元素压入到操作数栈顶&#xff0c;就是将j&#61;0压入操作数栈顶11: iinc 1, 1 // 将局部变量表中的1号元素自增1&#xff0c;此时局部变量表中的j&#61;114: istore_1 // 将操作数栈顶的元素&#xff08;此时栈顶元素为0&#xff09;弹出并赋值给局部变量表中的1号 槽位&#xff08;一号槽位本来已经完成自增了&#xff0c;但是又被赋值成了0&#xff09;15: iinc 2, 1 // 将局部变量表中的2号槽位的元素自增1&#xff0c;此时局部变量表中的2号元素值为1&#xff0c;也就是i&#61;118: goto 4 // 第一次循环结束&#xff0c;跳转到第四行继续循环21: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;24: iload_125: invokevirtual #3 // Method java/io/PrintStream.println:(I)V28: return

我们着重关注第10&#xff0c;11&#xff0c;14行字节码指令&#xff0c;用图表示如下&#xff1a;

在这里插入图片描述

可以看到本来局部变量表中的j已经完成了自增&#xff08;iinc指令是直接对局部变量进行自增&#xff09;&#xff0c;但是在进行赋值时是将操作数栈中的数据弹出&#xff0c;但是操作数栈的数据并没有经过计算&#xff0c;所以每次自增的结果都被覆盖了。最终结果就是0。

我们平常说的i&#43;&#43;是先拿去用&#xff0c;然后再自增&#xff0c;而&#43;&#43;i是先自增再拿去用。这个到底怎么理解呢&#xff1f;如果站在JVM的层次来讲的话&#xff0c;应该这样说&#xff1a;

i&#43;&#43;是先被操作数栈拿去用了&#xff08;先执行的load指令&#xff09;&#xff0c;然后再在局部变量表中完成了自增&#xff0c;但是操作数栈中还是自增前的值
而&#43;&#43;1是先在局部变量表中完成了自增&#xff08;先执行innc指令&#xff09;&#xff0c;然后再被load进了操作数栈&#xff0c;所以操作数栈中保存的是自增后的值
这就是它们的根本区别。

最后我这里放出一段代码及其字节码

public static void main(String[] args) {int i &#61; 4;int b &#61; i&#43;&#43;;int a &#61; &#43;&#43;i;
}public static void main(java.lang.String[]);
Code:0: iconst_41: istore_12: iload_13: iinc 1, 16: istore_27: iinc 1, 110: iload_111: istore_312: return

这段代码大家自行思考&#xff0c;有任何问题可以给我留言哦~

码字不易&#xff0c;记得点个赞哈~

PS&#xff1a;图中局部变量表的下标都是从1开始&#xff0c;这是因为我直接用main函数测试的&#xff0c;局部变量表中下标为0的元素是main函数中的形参&#xff0c;也就是String[]args。另外也通过这些过程我们也可以发现&#xff0c;局部变量表就是通过下标访问的&#xff0c;而操作数栈就是通过正常的栈操作&#xff08;压入/弹出&#xff09;来完成数据访问的。


推荐阅读
  • 如何搭建Java开发环境并开发WinCE项目
    本文介绍了如何搭建Java开发环境并开发WinCE项目,包括搭建开发环境的步骤和获取SDK的几种方式。同时还解答了一些关于WinCE开发的常见问题。通过阅读本文,您将了解如何使用Java进行嵌入式开发,并能够顺利开发WinCE应用程序。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • VScode格式化文档换行或不换行的设置方法
    本文介绍了在VScode中设置格式化文档换行或不换行的方法,包括使用插件和修改settings.json文件的内容。详细步骤为:找到settings.json文件,将其中的代码替换为指定的代码。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • 本文介绍了RPC框架Thrift的安装环境变量配置与第一个实例,讲解了RPC的概念以及如何解决跨语言、c++客户端、web服务端、远程调用等需求。Thrift开发方便上手快,性能和稳定性也不错,适合初学者学习和使用。 ... [详细]
  • CF:3D City Model(小思维)问题解析和代码实现
    本文通过解析CF:3D City Model问题,介绍了问题的背景和要求,并给出了相应的代码实现。该问题涉及到在一个矩形的网格上建造城市的情景,每个网格单元可以作为建筑的基础,建筑由多个立方体叠加而成。文章详细讲解了问题的解决思路,并给出了相应的代码实现供读者参考。 ... [详细]
  • Java中包装类的设计原因以及操作方法
    本文主要介绍了Java中设计包装类的原因以及操作方法。在Java中,除了对象类型,还有八大基本类型,为了将基本类型转换成对象,Java引入了包装类。文章通过介绍包装类的定义和实现,解答了为什么需要包装类的问题,并提供了简单易用的操作方法。通过本文的学习,读者可以更好地理解和应用Java中的包装类。 ... [详细]
  • 纠正网上的错误:自定义一个类叫java.lang.System/String的方法
    本文纠正了网上关于自定义一个类叫java.lang.System/String的错误答案,并详细解释了为什么这种方法是错误的。作者指出,虽然双亲委托机制确实可以阻止自定义的System类被加载,但通过自定义一个特殊的类加载器,可以绕过双亲委托机制,达到自定义System类的目的。作者呼吁读者对网上的内容持怀疑态度,并带着问题来阅读文章。 ... [详细]
  • Java SE从入门到放弃(三)的逻辑运算符详解
    本文详细介绍了Java SE中的逻辑运算符,包括逻辑运算符的操作和运算结果,以及与运算符的不同之处。通过代码演示,展示了逻辑运算符的使用方法和注意事项。文章以Java SE从入门到放弃(三)为背景,对逻辑运算符进行了深入的解析。 ... [详细]
  • 本文讨论了在VMWARE5.1的虚拟服务器Windows Server 2008R2上安装oracle 10g客户端时出现的问题,并提供了解决方法。错误日志显示了异常访问违例,通过分析日志中的问题帧,找到了解决问题的线索。文章详细介绍了解决方法,帮助读者顺利安装oracle 10g客户端。 ... [详细]
  • 本文整理了Java面试中常见的问题及相关概念的解析,包括HashMap中为什么重写equals还要重写hashcode、map的分类和常见情况、final关键字的用法、Synchronized和lock的区别、volatile的介绍、Syncronized锁的作用、构造函数和构造函数重载的概念、方法覆盖和方法重载的区别、反射获取和设置对象私有字段的值的方法、通过反射创建对象的方式以及内部类的详解。 ... [详细]
  • Java String与StringBuffer的区别及其应用场景
    本文主要介绍了Java中String和StringBuffer的区别,String是不可变的,而StringBuffer是可变的。StringBuffer在进行字符串处理时不生成新的对象,内存使用上要优于String类。因此,在需要频繁对字符串进行修改的情况下,使用StringBuffer更加适合。同时,文章还介绍了String和StringBuffer的应用场景。 ... [详细]
  • 本文介绍了C函数ispunct()的用法及示例代码。ispunct()函数用于检查传递的字符是否是标点符号,如果是标点符号则返回非零值,否则返回零。示例代码演示了如何使用ispunct()函数来判断字符是否为标点符号。 ... [详细]
  • 自动轮播,反转播放的ViewPagerAdapter的使用方法和效果展示
    本文介绍了如何使用自动轮播、反转播放的ViewPagerAdapter,并展示了其效果。该ViewPagerAdapter支持无限循环、触摸暂停、切换缩放等功能。同时提供了使用GIF.gif的示例和github地址。通过LoopFragmentPagerAdapter类的getActualCount、getActualItem和getActualPagerTitle方法可以实现自定义的循环效果和标题展示。 ... [详细]
  • 在Oracle11g以前版本中的的DataGuard物理备用数据库,可以以只读的方式打开数据库,但此时MediaRecovery利用日志进行数据同步的过 ... [详细]
author-avatar
手机用户2602891927
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有