本专栏是针对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$$
此时运算并没有结束,9会被重新压入栈中:
然后执行加法操作,将栈中的两个数弹出相加:
$$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是先自增,后赋值。
这种说法肯定是错误的,有这种想法的同学也应该及时改正过来,事实上,不管自增操作符在左边还是右边,赋值操作永远是最后进行的,而并不是说自增符在右边就先进行了赋值操作。
自增符号的位置不同所导致的结果值不同,是操作数栈导致的,自增符在左边则先自增再压入栈,此时弹出的肯定是自增后的值;而如果自增符在右边,则先压入栈再自增,此时弹出的值还是原来的值,这才是这个问题的根本原因。
总结
通过该问题,总结以下几点:
- 赋值操作永远是最后进行
- 等号右边的值按从左到右的顺序依次压入操作数栈
- 具体先算哪个, 要根据运算符的优先级
- 自增、自减操作都是直接修改变量的值,不经过操作数栈
- 在赋值之前,运算得到的临时结果仍然存储在操作数栈中