问题重现
请看下面这段代码,最终两个add方法的返回值各是多少?
public static void main(String[] args) {
int result1 = add1();
int result2 = add2();
System.out.println("add1:"+ result1 );
System.out.println("add2:"+ result2 );
}
public static int add1(){
int i =0;
i=i++;
return i;
}
public static int add2(){
int i =0;
i=++i;
return i;
}
直接说结果:
1735314267983.png
这应该是每个刚学java的程序员都可能遇到过的问题,那时候老师只告诉了i=i++ 不会改变i的值,如果需要这个值先加运算需要使用++i,要记住这个点,其他的具体原因没有说。
最近在研究JVM的字节码文件,终于找到该问题的原因。
方法字节码i++
分析
通过jclasslib
查询 add1方法的字节码文件发现,add1方法字节码源码如下
add1方法字节码.png
字节码对应到代码中是这样的:
public int add1() {
int i = 0; // 对应于 iconst_0 和 istore_0
i = i++; // 对应于iload、 iinc和 istore
return i; // 对应于 iload 和 ireturn
}
步骤 | 栈状态 | 本地变量表状态 | 说明 |
---|---|---|---|
初始化i | [ ] | [i = 0] | 通过iconst_0和istore_0初始化i |
加载i(iload_0) | [1] | [i = 0] | 将本地变量表i值加载到栈 |
自增(iinc) | [ ] | [i = 1] | 自增本地变量表i值 |
覆盖i(istore_0) | [ ] | [i = 0] | 将栈顶值存储回本地变量表i |
再次加载i(iload_0) | [0] | [i = 0] | 再次加载变量i |
返回值(ireturn) | [ ] | [i = 0] | 返回栈顶值 |
方法字节码i++
分析
通过jclasslib
查询 add1方法的字节码文件发现,add2方法字节码源码如下
add2方法字节码.png
字节码对应到代码中是这样的:
public int add1() {
int i = 0; // 对应于 iconst_0 和 istore_0
i = ++i; // 对应于 iinc、iload 和 istore
return i; // 对应于 iload 和 ireturn
}
步骤 | 栈状态 | 本地变量表状态 | 说明 |
---|---|---|---|
初始化i | [ ] | [i = 0] | 通过iconst_0和istore_0初始化i |
自增(iinc) | [ ] | [i = 1] | 自增本地变量表i值 |
加载i(iload_0) | [1] | [i = 1] | 将本地变量表i值加载到栈 |
覆盖i(istore_0) | [ ] | [i = 1] | 将栈顶值存储回本地变量表i |
再次加载i(iload_0) | [1] | [i = 1] | 再次加载变量i |
返回值(ireturn) | [ ] | [i = 1] | 返回栈顶值 |
总结
对比一下两者的区别就是:
-
i++
是在先将i的值加载到操作数栈中临时存储,然后再对本地变量表的i进行+1
操作,然后再将操作数栈的数据加载回本地,这样导致了+1
操作被回滚了 -
++i
的字节码操作顺序是先将本地变量i
进行+1
操作,这样本地变量的值就变成了1,然后才加载到操作数栈中,然后执行赋值操作 将操作数栈的数据加载回本地变量i,相当于是先执行了+1
操作然后进行赋值。
至此我们就能理解为什么i=i++ i的值没有变化,而i=++i有变化了,它们两个方法的本质区别就是i++
与++i
在转成字节码文件时的执行命令的顺序不一样。
操作 | 步骤顺序 | 结果差异 |
---|---|---|
i = i++ |
1. 读取旧值 -> 2. 自增本地变量 -> 3. 使用旧值赋值 | 最终值等于自增前的旧值 |
i = ++i |
1. 自增本地变量 -> 2. 读取新值 -> 3. 使用新值赋值 | 最终值等于自增后的新值 |