i = i++
一、 从一道笔试题说起
问:下列语句的输出结果是多少
public class Integer_int {
public static void main(String[] args) {
int i = 0;
i = i++;
System.out.println(i);
}
}
根据自增后缀形式的语法,i++
是先将i的值给外层表达式,然后将i的值加1。按照这种解释,那i = i++
的执行过程便是:
- 先将i的初始值0赋值给i,此时i的值是0
- 然后i的值加1,此时i的值是1
按照上面的过程,输出结果应该是1。可是,实际运行之后上面的输出结果是0,i的值并没有改变!!!
二、 前置知识
要想彻底理解i=i++
的执行流程,我们必须从虚拟机层面对其进行探究。在这之前,需要简单了解以下几个前置知识:
2.1 局部变量表和操作数栈
在JVM中,每个方法都会有一个对应的方法调用帧栈,方法调用帧栈包含以下两个重要组成部分(还包含其他部分,但这里只需要知道这两个即可):
- 局部变量表:是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量
- 局部变量表的索引从0开始
- 对于非静态方法,局部变量表索引为0的位置用于存储对当前对象的引用,即
this
关键字所引用的对象 - 对于静态方法,包括
main
方法,局部变量表索引为0的位置并没有特殊的用途,它像其他索引位置一样,用于存储方法的参数或局部变量 - 在
main
方法中,索引为0的位置通常用于存储传递给方法的第一个参数,即字符串数组args
- 操作数栈:用于存储计算过程中的中间值
2.2 一些JVM指令的简介
- iconst_0:iconst是一个入栈指令,用于将 int 类型的数压入操作数栈。比如iconst_0便是将 int 类型常量 0 压入操作数栈栈顶
- istore_1:istore的作用是将操作数栈栈顶的数据弹出,并将其存储到局部变量表中相应的变量中。比如istore_1便是将操作数栈栈顶的数据弹出,然后存放到局部变量表中索引为1的变量中
- iload_1:iload的作用是将局部变量表中相应位置的 int 类型变量的值加载到操作数栈栈顶。比如iload_1的作用便是将局部变量表中索引为1的 int 值加载到操作数栈顶
- iinc 1 by 1:innc的作用是对局部变量进行自增减操作,它可以将指定的 int 类型的局部变量增加或减少指定的值。 比如iinc 1 by 1的作用便是将局部变量表中索引为1的 int 变量的值增加1,这条指令通常用于实现变量自增的操作
三、 从JVM角度看 i = i++
public class Integer_int {
public static void main(String[] args) {
int i = 0;
i = i++;
System.out.println(i);
}
}
将上述代码进行编译,然后对得到的字节码文件进行反编译可以得到 main 函数对应的指令:
0 iconst_0
1 istore_1
2 iload_1
3 iinc 1 by 1
6 istore_1
7 return
为了方便理解,结合以下示意图对其进行说明:
-
0 iconst_0:
将整数 0 压入操作数栈栈顶
-
1 istore_1:
将操作数栈栈顶的整数(0)弹出,并放入局部变量表中索引为 1 的变量,即将0赋值给变量 i(索引为 0 的地方用于存放 main 函数的参数 args)
-
2 iload_1:
将局部变量表中索引为 1 的变量的值(即 i 的值0)取出压入操作数栈栈顶,此时操作数栈栈顶的值是0
-
3 iinc 1 by 1
将局部变量表中索引为 1 的变量的值(即 i 的值0)加1,此时局部变量表中 i 的值是1
-
6 istore_1
将操作数栈栈顶的整数(0)弹出,并放入局部变量表中索引为 1 的变量,即将局部变量表中 i 的值修改为 0,此时 i 的值又变成了0
其中:
- 步骤1以及步骤2完成了变量i的初始化工作,即对应的是语句
int i = 0;
- 步骤3~步骤4对应的是语句
i = i++;
四、 再看题目
从上面的JVM指令可以清晰的看到,虽然i的值也经过了自增的操作,即3 iinc 1 by 1
。但继续往下执行,当执行到第五步6 istore_1
时,i的值又被修改回了最初的值。因此 i = i++
并不会修改 i 的值!!!
五、 从JVM角度看 i = ++i
将上面代码中的i = i++
改为i = ++i
,编译之后反编译得到以下指令:
0 iconst_0
1 istore_1
2 iinc 1 by 1
5 iload_1
6 istore_1
7 return
同样,结合以下示意图对其进行说明:
-
0 iconst_0:
将整数 0 压入操作数栈栈顶
-
1 istore_1:
将操作数栈栈顶的整数(0)弹出,并放入局部变量表中索引为 1 的变量,即将0赋值给变量 i(索引为 0 的地方用于存放 main 函数的参数 args)
-
2 iinc 1 by 1
将局部变量表中索引为 1 的变量的值(即 i 的值0)加1,此时局部变量表中 i 的值是1
-
5 iload_1:
将局部变量表中索引为 1 的变量的值(即 i 的值1)取出压入操作数栈栈顶,此时操作数栈栈顶的值是1
-
6 istore_1
将操作数栈栈顶的整数(1)弹出,并放入局部变量表中索引为 1 的变量,此时 i 的值还是1
其中:
- 步骤1以及步骤2完成了变量i的初始化工作,即对应的是语句
int i = 0;
- 步骤3~步骤4对应的是语句
i = ++i;
六、 自增操作再理解
基于上面的理解,我们使用表格对比了k = i++
和k = ++i
的执行过程:
k = i++ | k = ++i | |
---|---|---|
第一步 | 将局部变量表中i的值放入操作数栈 | 将局部变量表中i的值加1 |
第二步 | 将局部变量表中i的值加1 | 将局部变量表中i的值放入操作数栈 |
第三步 | 将操作数栈中的值赋值给k | 将操作数栈中的值赋值给k |
从上面的表格可以看出,在JVM的角度,变量 i 中存放的值并不是直接赋值给了 k, 而是经过了操作数栈。只是:
- 对于
i++
,是先将 i 的值放入操作数栈再自增。因此,k 从操作数栈中获取到的是未自增前的 i 的值,这也正好对应了语法层面上 i 先将值传给外部表达式再自增的效果 - 对于
++i
,是 i 的值先自增再将自增后的值放入操作数栈中。因此,k 从操作数栈中获取到的是自增后的 i 的值,这也正好对应了语法层面上 i 先自增再将自增后的值传给外部表达式的效果
进一步地,当把k换成i后,也便有了i = i++
执行完之后 i 的值不变的情况,因为最后 i 的值是来自操作数栈,而操作数栈中的值是 i 未自增前的值。
七、 笔/面试题目
-
问:变量 i 的初始值为10,分别执行完以下语句后 i 的值是多少
A. i = i++; B. i = ++i
答:根据上面的内容,可知:执行完语句 A 后 i 的值不变仍然是10,执行完语句 B 后 i 的值为 11。
-
问:下列语句的输出结果
public class Integer_int { public static void main(String[] args) { int i = 10; i = i++ + ++i + i++ + ++i; System.out.println(i); } }
答:
- 首先,确定
i = i++ + ++i + i++ + ++i
的执行顺序。在Java中,自增运算符的优先级高于加法运算符,而赋值运算符的优先级最低。因此,可以确定该语句的执行顺序为:- 先执行自增操作
- 将每次自增操作的返回值相加
- 将相加结果赋值给i
- 计算过程如下
- 当第一个
i++
执行完后,返回10 ,i的值自增为11 - 当第一个
++i
执行完后,i的值自增为12,返回12 - 当第二个
i++
执行完后,返回12,i的值自增为13 - 当第二个
++i
执行完后,i的值自增为14,返回14 - 将上述结果的返回值相加:10+12+12+14=48
- 当第一个
- 最终结果为48
- 首先,确定