飞道的博客

i++和++i傻傻分不清楚?这里给你最清楚的解答

372人阅读  评论(0)

本专栏是针对Java面试题打造的一款专栏,每篇文章对应一个面试的常见问题,希望对大家有所帮助。

本篇文章将介绍——自增变量,这是面试常见的问题,说难不难,说简单也不简单,需要面试者冷静思考,判断正确符号之间的优先级。

看下面的一个程序:

	@Test
	public void test1(){
		int i = 1;
		i = i++;
		int j = i++;
		int k = i + ++i * i++;
		System.out.println("i = " + i);		
		System.out.println("j = " + j);		
		System.out.println("k = " + k);
	}

大家先别急着去运行看答案,先自己分析一下,这段程序究竟会输出什么?

我们先分析一下最简单的,变量j会输出什么?
程序中改变变量j的值只有一个地方:

int j = i++;

这个相信难不倒大家吧,相信大家也没少被这种问题坑过,但是吃一堑,长一智。对于这行代码,因为自增符号++在变量i的右边,所以j的值一定是 1。

至于i和j的值到底是多少,我先给出答案:

i = 4
j = 1
k = 11

有没有同学看到答案后感到怀疑人生了呢?没有的话那首先应该恭喜你,你有足够扎实的基本功,这道题没有难倒你。

也有同学看到答案后深知自己答错了,但是回过头来看代码突然又明白了,这样的同学相信你们也是能够答对这道题的,只是因为粗心大意,所以在做这种类型的题目时要非常小心,这些题目里肯定是挖了坑等着你跳的。

应该还有部分同学是处于持续懵逼状态的吧,不用担心,这篇文章就是为你量身打造的,看完这篇文章后,在应对这种题目时一定是"手到擒来"。

代码分析

关于j的值,前面已经分析过了,我们重点分析一下变量i和k。

首先程序定义了一个变量i = 1,紧接着进行了赋值操作:

i = i++;

由于自增符号在右边,所以i 的值为1,刚才的变量j也是这么算的,那么到底为什么自增符号在右边,i的值就一定为1呢?

我们先要来了解一个概念——栈帧

栈帧(stack frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。
每一个方法从调用开始到执行完成的过程,就对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

当程序执行int i = 1;后,在局部变量表中便存放了变量i的值为1(局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量)。

当程序执行第二行代码i = i++;时,操作数栈就要发挥作用了(操作数栈可理解为java虚拟机栈中的一个用于计算的临时数据存储区),java虚拟机把操作数栈作为它的工作区——大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。

比如这里的操作,它首先将i的值压入操作数栈中,此时i自增,这时候局部变量表中的i值为2,此时执行赋值操作,需要将操作数栈中的值弹出来再赋值给i,这样操作数栈中的值1则又覆盖了变量i,所以i仍然为1(j的计算方式同理)。

这里引申一下,如果改为i = ++i,那么i的值就为2,因为执行该代码,局部变量表中的i会先自增为2,然后再被压入操作数栈中,此时执行赋值操作,弹出来的值就为2了。

前两行代码执行过后,i的值为1,然后执行第三行代码:

int j = i++;

这行代码虽然没有改变j的值,但是局部变量表中i的值是发生变化了的,i变为了2。

再看第四行代码:

int k = i + ++i * i++;

这也是最关键的一行代码,我们画图来理解一下:

首先在局部变量表中有一个变量k,它的值是等号右边的运算结果。


首先会将i的值压入操作数栈:

先乘除后加减,首先执行++i * i++,先看++i操作,因为自增符号在左边,所以先自增,此时局部变量表中的i值为3,再将其压入操作数栈:

再执行i++操作(自增运算优先级高于乘法运算),此时因为自增符在右边,所以先将i的值压入操作数栈,再自增:

接着就要进行乘法操作了,将操作数栈中的两个数弹出进行乘法操作:
3 3 = 9 3 * 3 = 9
此时运算并没有结束,9会被重新压入栈中:

然后执行加法操作,将栈中的两个数弹出相加:
9 + 2 = 11 9 + 2 = 11
同样的,此时运算还没结束,11被重新压入栈中:

最后执行赋值操作,将栈中的值11弹出,并赋值给局部变量表中的变量k,此时k的值为11。

以上就是变量i和k的计算过程。

整个计算过程我们可以通过查看字节码文件知晓,在Dos窗口输入指令:

javap -verbose 类名

Dos窗口便会输出具体的执行过程,这里我贴出最重要的部分:

public static void main(java.lang.String[]);
  descriptor: ([Ljava/lang/String;)V
  flags: ACC_PUBLIC, ACC_STATIC
  Code:
    stack=3, locals=4, args_size=1
       0: iconst_1
       1: istore_1
       2: iload_1
       3: iinc          1, 1
       6: istore_1
       7: iload_1
       8: iinc          1, 1
      11: istore_2
      12: iload_1
      13: iinc          1, 1
      16: iload_1
      17: iload_1
      18: iinc          1, 1
      21: imul
      22: iadd

看第一步,也就是标号0的指令:iconst_1,有JVM指令基础的同学应该能够看懂吧,不懂的话可以百度查一查,该指令的意思是将一个常量加载到操作数栈中;
标号1的指令:istore_1,意思是将一个数值从操作数栈弹出存储到局部变量表,所以这两个指令共同完成了语句int i = 1

再看标号2的指令:iload_1,该指令将一个本地变量加载到操作数栈中,
标号3的指令:iinc,该指令会对指定变量进行加一个值的操作,
然后是标号6的指令:istore_1,该指令又将一个数值从操作数栈中弹出存储到局部变量表,这三条指令共同完成了语句i = i++

后面的我就不分析了,大家可以自己看看后面的运行过程是否和前面分析的一样。

需要注意的地方

看到很多文章上都写着:i++是先赋值,然后再自增;++i是先自增,后赋值。
这种说法肯定是错误的,有这种想法的同学也应该及时改正过来,事实上,不管自增操作符在左边还是右边,赋值操作永远是最后进行的,而并不是说自增符在右边就先进行了赋值操作。

自增符号的位置不同所导致的结果值不同,是操作数栈导致的,自增符在左边则先自增再压入栈,此时弹出的肯定是自增后的值;而如果自增符在右边,则先压入栈再自增,此时弹出的值还是原来的值,这才是这个问题的根本原因。

总结

通过该问题,总结以下几点:

  • 赋值操作永远是最后进行
  • 等号右边的值按从左到右的顺序依次压入操作数栈
  • 具体先算哪个, 要根据运算符的优先级
  • 自增、自减操作都是直接修改变量的值,不经过操作数栈
  • 在赋值之前,运算得到的临时结果仍然存储在操作数栈中
微信搜索【码视界】或者扫描下方二维码关注我的微信公众号,更多精彩等着你!

转载:https://blog.csdn.net/qq_42453117/article/details/104247031
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场