面试官:你说你懂i++跟++i的区别,那你会做下面这道题吗?

简介: 面试官:你说你懂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的运行时数据区域是分为好几块的,具体分布如下图所示:

在这里插入图片描述


微信图片_20221113125216.png

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


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

局部变量表

操作数栈

方法出口

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

20200407000105901.png

其中的局部变量表存放了编译期可知的各种基本数据类型(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;
}

这个过程如下所示

20200407000113960.png

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

20200407000119578.png

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

20200415150837173.png

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

image.png

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行字节码指令,用图表示如下:

2020040700014127.png

可以看到本来局部变量表中的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。另外也通过这些过程我们也可以发现,局部变量表就是通过下标访问的,而操作数栈就是通过正常的栈操作(压入/弹出)来完成数据访问的



相关文章
|
4月前
|
Java
【Java集合类面试二十八】、说一说TreeSet和HashSet的区别
HashSet基于哈希表实现,无序且可以有一个null元素;TreeSet基于红黑树实现,支持排序,不允许null元素。
|
4月前
|
Java
【Java集合类面试二十三】、List和Set有什么区别?
List和Set的主要区别在于List是一个有序且允许元素重复的集合,而Set是一个无序且元素不重复的集合。
|
4月前
|
存储 Java 索引
【Java集合类面试二十四】、ArrayList和LinkedList有什么区别?
ArrayList基于动态数组实现,支持快速随机访问;LinkedList基于双向链表实现,插入和删除操作更高效,但占用更多内存。
|
23天前
|
Java 程序员
Java社招面试题:& 和 && 的区别,HR的套路险些让我翻车!
小米,29岁程序员,分享了一次面试经历,详细解析了Java中&和&&的区别及应用场景,展示了扎实的基础知识和良好的应变能力,最终成功获得Offer。
56 14
|
3月前
|
Android开发 Kotlin
Android经典面试题之Kotlin的==和===有什么区别?
本文介绍了 Kotlin 中 `==` 和 `===` 操作符的区别:`==` 用于比较值是否相等,而 `===` 用于检查对象身份。对于基本类型,两者行为相似;对于对象引用,`==` 比较值相等性,`===` 检查引用是否指向同一实例。此外,还列举了其他常用比较操作符及其应用场景。
196 93
|
21天前
|
Java 关系型数据库 数据库
京东面试:聊聊Spring事务?Spring事务的10种失效场景?加入型传播和嵌套型传播有什么区别?
45岁老架构师尼恩分享了Spring事务的核心知识点,包括事务的两种管理方式(编程式和声明式)、@Transactional注解的五大属性(transactionManager、propagation、isolation、timeout、readOnly、rollbackFor)、事务的七种传播行为、事务隔离级别及其与数据库隔离级别的关系,以及Spring事务的10种失效场景。尼恩还强调了面试中如何给出高质量答案,推荐阅读《尼恩Java面试宝典PDF》以提升面试表现。更多技术资料可在公众号【技术自由圈】获取。
|
1月前
|
存储 缓存 网络协议
计算机网络常见面试题(二):浏览器中输入URL返回页面过程、HTTP协议特点,GET、POST的区别,Cookie与Session
计算机网络常见面试题(二):浏览器中输入URL返回页面过程、HTTP协议特点、状态码、报文格式,GET、POST的区别,DNS的解析过程、数字证书、Cookie与Session,对称加密和非对称加密
|
2月前
|
编译器
经典面试题:变量的声明和定义有什么区别
在编程领域,变量的“声明”与“定义”是经典面试题之一。声明告诉编译器一个变量的存在,但不分配内存,通常包含变量类型和名称;而定义则为变量分配内存空间,一个变量必须至少被定义一次。简而言之,声明是告知变量形式,定义则是实际创建变量并准备使用。
|
2月前
|
XML 前端开发 Java
Spring,SpringBoot和SpringMVC的关系以及区别 —— 超准确,可当面试题!!!也可供零基础学习
本文阐述了Spring、Spring Boot和Spring MVC的关系与区别,指出Spring是一个轻量级、一站式、模块化的应用程序开发框架,Spring MVC是Spring的一个子框架,专注于Web应用和网络接口开发,而Spring Boot则是对Spring的封装,用于简化Spring应用的开发。
180 0
Spring,SpringBoot和SpringMVC的关系以及区别 —— 超准确,可当面试题!!!也可供零基础学习
|
2月前
|
前端开发 小程序 JavaScript
面试官:px、em、rem、vw、rpx 之间有什么区别?
面试官:px、em、rem、vw、rpx 之间有什么区别?
59 0