面试官:你说你懂i++跟++i的区别,那你知道下面这段代码的运行结果吗?

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

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

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

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

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

面试官:

我:“哈哈…,开个玩笑,结果为0啦”

面试官:“你能说说为什么吗?”

我:“因为j++这个表达式每次返回的都是0,所以最终结果就是0”

面试官:“小伙子不错,那你能从JVM的角度讲一讲为什么嘛?”

我心想:这货明显是在搞事情啊,这么快就到JVM了?还好我有准备。

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

在这里插入图片描述



现在我们主要关注其中的虚拟机栈,关于虚拟机栈,我们知道它有以下几个特点:

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

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

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

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

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

以执行下面这段代码为例:

public static  void main(String[] args){
int a = 2;
int b = 3;
int c = a + b;
}

这个过程如下所示

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

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

到这里还没有完哦,还剩最后一步,需要将计算后的结果赋值给c,也就是要将操作数栈的数据弹出并赋值给局部变量表中的第三个槽位

OK,到这一步整个过程就完成了

面试官:“嗯,说的不错,但是你还是没解释为什么最开始的那个问题,为什么j=j++的结果会是0呢?”

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

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

// 为方便阅读将对应代码也放到这里
public static void main(String[] args) {
int j = 0;
for (int i = 0; i < 10; i++) {
j = (j++);
}
System.out.println(j);
}
  public static void main(java.lang.String[]);
Code:
0: iconst_0 // 将常数0压入到操作数栈顶
1: istore_1 // 将操作数栈顶元素弹出并压入到局部变量表中1号槽位,也就是j=0
2: iconst_0 // 将常数0压入到操作数栈顶
3: istore_2 // 将操作数栈顶元素弹出并压入到局部变量表中2号槽位,也就是i=0
4: iload_2 // 将2号槽位的元素压入操作数栈顶
5: bipush 10 // 将常数10压入到操作数栈顶,此时操作数栈中有两个数(常数10,以及i)
7: if_icmpge 21 // 比较操作数栈中的两个数,如果i>=10,跳转到第21行
10: iload_1 // 将局部变量表中的1号槽位的元素压入到操作数栈顶,就是将j=0压入操作数栈顶
11: iinc 1, 1 // 将局部变量表中的1号元素自增1,此时局部变量表中的j=1 14: istore_1 // 将操作数栈顶的元素(此时栈顶元素为0)弹出并赋值给局部变量表中的1号 槽位(一号槽位本来已经完成自增了,但是又被赋值成了0) 15: iinc 2, 1 // 将局部变量表中的2号槽位的元素自增1,此时局部变量表中的2号元素值为1,也就是i=1 18: goto 4 // 第一次循环结束,跳转到第四行继续循环
21: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
24: iload_1
25: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
28: return

我们着重关注第10,11,14行字节码指令,用图表示如下:

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

我们平常说的i++是先拿去用,然后再自增,而++i是先自增再拿去用。这个到底怎么理解呢?如果站在JVM的层次来讲的话,应该这样说:

  1. i++是先被操作数栈拿去用了(先执行的load指令),然后再在局部变量表中完成了自增,但是操作数栈中还是自增前的值
  2. 而++1是先在局部变量表中完成了自增(先执行innc指令),然后再被load进了操作数栈,所以操作数栈中保存的是自增后的值

这就是它们的根本区别。

最后我这里放出一段代码及其字节码,我相信看完这篇文章你对于i++及++i的理解绝对跟原来不一样了

public static void main(String[] args) {
int i = 4;
int b = i++;
int a = ++i;
} public static void main(java.lang.String[]);
Code:
0: iconst_4
1: istore_1
2: iload_1
3: iinc 1, 1
6: istore_2
7: iinc 1, 1
10: iload_1
11: istore_3
12: return

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

码字不易,记得点个赞哈~

ps:

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

面试官:你说你懂i++跟++i的区别,那你会做下面这道题吗?的更多相关文章

  1. [每日一题]面试官问:for in和for of 的区别和原理?

    关注「松宝写代码」,精选好文,每日一题 ​时间永远是自己的 每分每秒也都是为自己的将来铺垫和增值 作者:saucxs | songEagle 一.前言 2020.12.23 日刚立的 flag,每日一 ...

  2. 面试官疯狂问我:char和varchar的区别 怎么办?愣着干嘛?进来白嫖啊!

    MySQL的修仙之路,图文谈谈如何学MySQL.如何进阶!(已发布) 面前突击!33道数据库高频面试题,你值得拥有!(已发布) 大家常说的基数是什么?(已发布) 讲讲什么是慢查!如何监控?如何排查?( ...

  3. 你所不知道的 CSS 阴影技巧与细节 滚动视差?CSS 不在话下 神奇的选择器 :focus-within 当角色转换为面试官之后 NPOI 教程 - 3.2 打印相关设置 前端XSS相关整理 委托入门案例

    你所不知道的 CSS 阴影技巧与细节   关于 CSS 阴影,之前已经有写过一篇,box-shadow 与 filter:drop-shadow 详解及奇技淫巧,介绍了一些关于 box-shadow  ...

  4. 我以为我对Mysql索引很了解,直到我遇到了阿里的面试官

    GitHub 4.8k Star 的Java工程师成神之路 ,不来了解一下吗? GitHub 4.8k Star 的Java工程师成神之路 ,真的不来了解一下吗? GitHub 4.8k Star 的 ...

  5. [每日一题]面试官问:谈谈你对ES6的proxy的理解?

    [每日一题]面试官问:谈谈你对ES6的proxy的理解? 关注「松宝写代码」,精选好文,每日一题 作者:saucxs | songEagle 一.前言 2020.12.23 日刚立的 flag,每日一 ...

  6. Redis——面试官考题

    总结: 本文在一次面试的过程中讲述了 Redis 是什么,Redis 的特点和功能,Redis 缓存的使用,Redis 为什么能这么快,Redis 缓存的淘汰策略,持久化的两种方式,Redis 高可用 ...

  7. 对线面试官,凭借nginx能一战封神吗?

    面试官:小伙子,你对nginx熟悉吗? 我:当然熟悉了,请听我慢慢道来. 心里想,我能吊打面试官吗?今天非得灭一灭面试官的威风,平时都被怼的狗血淋头. 面试官:就你那点花花肠子,咱还不清楚. 我:.. ...

  8. 面试官:RabbitMQ过期时间设置、死信队列、延时队列怎么设计?

    哈喽!大家好,我是小奇,一位不靠谱的程序员 小奇打算以轻松幽默的对话方式来分享一些技术,如果你觉得通过小奇的文章学到了东西,那就给小奇一个赞吧 文章持续更新 一.前言 RabbitMQ我们经常的使用, ...

  9. 面试官:BIO、NIO、AIO是什么,他们有什么区别?

    哈喽!大家好,我是小奇,一位热爱分享的程序员 小奇打算以轻松幽默的对话方式来分享一些技术,如果你觉得通过小奇的文章学到了东西,那就给小奇一个赞吧 文章持续更新 一.前言 书接上回,感觉上次的公司氛围不 ...

随机推荐

  1. 数据结构和算法(Golang实现)(6)简单入门Golang-并发、协程和信道

    并发.协程和信道 Golang语言提供了go关键字,以及名为chan的数据类型,以及一些标准库的并发锁等,我们将会简单介绍一下并发的一些概念,然后学习这些Golang特征知识. 一.并发介绍 我们写程 ...

  2. AJ学IOS(48)多线程网络之多线程简单了解

    AJ分享,必须精品 一:进程和线程 1:什么是进程 进程是指在系统中正在运行的一个应用程序 每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内. 比如同时打开QQ.Xcode,系统就会分 ...

  3. 你只要5行代码,拥有你的个性二维码,用Python生成动态二维码

    如果想了解更多关于python的应用,可以私信我,或者点击下方链接自行获取,里面到资料都是免费的(http://t.cn/A6Zvjdun) 二维码满天飞,但是有没有想过Python也能制作出专属于自 ...

  4. 一口气带你踩完五个 List 的大坑,真的是处处坑啊!

    List 可谓是我们经常使用的集合类之一,几乎所有业务代码都离不开 List.既然天天在用,那就没准就会踩中这几个 List 常见坑. 今天我们就来总结这些常见的坑在哪里,捞自己一手,防止后续同学再继 ...

  5. 百度智能云虚拟主机 Typecho 分类功能失效 | 开启伪静态地址

    出现的问题 $this->is() 方法失效,无法正确判断 archive.category.tags 页面类型. 点击分类页面.归档页面时,虽然 URL 是正确的,但网页内容却是 index. ...

  6. Linux - centos7.X安装tomcat8

    创建tomcat安装路径 mkdir /usr/local/tomcat wget直接下载tomcat8 注意,需要已经安装了wget命令 wget http://mirrors.estointern ...

  7. 用百度AI平台接口实现OCR文字识别

    目录 一.接入指南 1.注册 2.登录 3.创建应用 二.安装接口模型 三.编写python代码 四.识别结果 一.接入指南 若想利用百度AI开放平台进行软件开发,首先应成为百度AI开放平台的开发者. ...

  8. mybatis源码学习(一):Mapper的绑定

    在mybatis中,我们可以像下面这样通过声明对应的接口来绑定XML中的mapper,这样可以让我们尽早的发现XML的错误. 定义XML: <?xml version="1.0&quo ...

  9. Nginx重写请求后将url?后的参数去除

    2019独角兽企业重金招聘Python工程师标准>>> 使用?结尾     注意,关键点就在于"?"这个尾缀.重定向的目标地址结尾处如果加了?号,则不会再转发传递 ...

  10. dijkstra preiority_queue优化 紫书学习

    #include<bits/stdc++.h> using namespace std; const int maxn=1000+10; const int INF=1000000000; ...