i++问题引发的知识链
一个表面上简单的i++问题,让我从此带着敬畏之心去学习!
翻出自己的笔记,现在重新看一遍还是有些收获了。自己写的东西,看起来的确容易回想起来。写于2015-09-04 11:14:54
例子
public class SumPlusTest {
public static void main(String[] args){
int a = 1, b = 1, c = 1, d =1;
a++;
++b;
c = c++;
d = ++d;
System.out.println("a : " + a + "\nb : " + b + "\nc : " + c + "\nd : " + d);
}
}
输出结果
a : 2
b : 2
c : 1
d : 2
分析
a、b、d三个参数的结果都没什么问题,暂且放一边,主要问题出来c上,为什么结果是1,而不是2?
有两种解释方法,其实最后本质是一样的。
解法一
因为Java用了中间缓存变量机制,所有c=c++可换成如下写法:
tmp = c;
c = c++;
c = tmp;
看着其实比较明确了,但是说实话其实没看懂,什么是中间缓存变量机制,为什么要有这个,官方文档在哪?
这些问题不是很明了,国外的权威资料暂时也没得求证,所以索性从字节码的角度来理解这个问题。
解法二
通过javap
工具查看虚指令,通过虚指令理解c=c++到底做了什么。
下面结果只保留部分主要内容来说明c=c++
和其它几种情况的不同,直接翻译虚指令对应程序中的具体操作。
[root@z1 classdir]# javap -verbose SumPlusTest
Classfile /mnt/workspace/java/i++issue/classdir/SumPlusTest.class
Last modified Sep 3, 2015; size 794 bytes
MD5 checksum 3a406dc81fe69722d53ce74ef96516a4
public class SumPlusTest
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #14.#30 // java/lang/Object."<init>":()V
#2 = Fieldref #31.#32 // java/lang/System.out:Ljava/io/PrintStream;
......
#54 = Utf8 println
#55 = Utf8 (Ljava/lang/String;)V
{
public SumPlusTest();
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LSumPlusTest;
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=5, args_size=1
0: iconst_1
1: istore_1 #0,1相当于是执行a=1
2: iconst_1
3: istore_2 #2,3相当于是执行b=1
4: iconst_1
5: istore_3 #4,5相当于是执行c=1
6: iconst_1
7: istore_4 #6,7相当于是执行d=1
9: iinc 1, 1 #相当于执行a的值+1
12: iinc 2, 1 #相当于执行b的值+1
15: iload_3 #将c的值放入栈顶
16: iinc 3, 1 #执行c的值+1
19: istore_3 #再将栈顶的值赋值给c,此时即c=1
20: iinc 4, 1 #相当于是执行d的值+1
23: iload 4 #把d的值放置于栈顶
25: istore 4 #然后将栈顶的值付给d这个变量,此时d=2
......
LocalVariableTable:
Start Length Slot Name Signature
0 81 0 args [Ljava/lang/String;
2 79 1 a I
4 77 2 b I
6 75 3 c I
9 72 4 d I
}
i++问题例子补充
我承认我钻牛角尖了,测试了几个更奇葩的例子。
例一
程序
int i = 0;
i = (i++) + (++i);
System.out.println("i : " + i);
结果
i=2
分析
0: iconst_0
1: istore_1
2: iload_1 #将数值0推入栈顶
3: iinc 1, 1
6: iinc 1, 1 ##自加两次
9: iload_1 #将数值2推入栈顶
10: iadd #将0和2相加,并将结果放入栈顶
11: istore_1 将i赋值为2
那么我就可以理解为第一个括号里的i++,其实相当于是一个tmp1=i++
的操作,同之前的例子一样,先将i推入栈顶,然后i自加。
第二个括号里的++i,i指向的是和前一个i的同一个slot,因此在原有的基础上继续自加,也就是变成了i=2。
由目前掌握的知识暂时得到一个结论,++
在前则先自增后推入栈,++
在后,则先推入栈顶,后自增。
例二
程序
int i = 0;
i = (i++) + (++i) + (i++);
System.out.println("i : " + i);
结果
先由已知的知识来推测执行过程:
- 先将i赋值为0,然后将该slot值推入栈顶,栈顶值为0
- i进行两次自加,此时slot值为2,推入栈顶,栈顶值为2
- 栈顶前两个int相加,结果存入栈顶,栈顶值为2
- 再将slot值推入栈顶,栈顶值为2
- 注意:slot的值自增1,但是没有推入栈顶。
- 栈顶前两个int相加,结果存入栈顶,栈顶值为4
- 结果是i值为4。
然后看实际是怎么操作的。
分析
0: iconst_0
1: istore_1
2: iload_1
3: iinc 1, 1
6: iinc 1, 1
9: iload_1
10: iadd
11: iload_1
12: iinc 1, 1
15: iadd
16: istore_1
整个执行的过程,基本和上面描述的一致。最后输出结果就是4。
例三
程序
再测试一个例子
int i = 0;
i = (++i) + (i++) + (++i);
System.out.println("i : " + i);
结果
再次根据已知的知识来推测执行过程:
- 先将i赋值为0,i自增1,然后推入栈顶,此时slot值为1,栈顶值为1
- 将slot值继续推入栈顶,此时slot值为1,栈顶值为1
- 栈顶前两个int相加,结果存入栈顶,此时slot值为1,栈顶值为2
- i指向的slot值自增1,此时slot值为2,栈顶值为2
- i指向的slot值自增1,此时slot值为3,栈顶值为2
- 将slot值推入栈顶,并且栈顶前两个int相加,结果存入栈顶,此时slot值为3,栈顶值为5
- 最后结果即为5
分析
0: iconst_0
1: istore_1
2: iinc 1, 1
5: iload_1
6: iload_1
7: iinc 1, 1
10: iadd
11: iinc 1, 1
14: iload_1
15: iadd
16: istore_1
从javap的结果可以看出来,有一个顺序错了,也就是第3,4两步的顺序需要颠倒一下,页就是说在程序里面第二个括号里面的i++
,先执行自加,然后栈顶的前两个元素才会相加。不过这个顺序小变化不影响最后结果。
总结
通过上面分析可以看出来,一个比较简单的i++问题,如果稍微深入一点学习的话,会涉及到不少平常不注意的内容。
对这一个知识点来说,为什么Java会这样来处理不太清楚,但是从结果来反推的话可以得到几个结论:
-
++
在前,则先自增后推入栈;++
在后,则先推入栈顶,后自增 - 如果
++
操作是针对同一个变量,那么不管是在前还是在后,其自增的值都会对后面的操作起到影响。
疑问
那么还是一些疑问。
即使通过虚指令明白了c = c++
的与众不同,但是还是不明白为什么会出现这种结果,Java在区分c = c++
和d = ++d
不同的时候是根据表达式的优先级来却别的吗?
还有就是相关的官方权威的资料一直没有找到。
再有一点就是,经过c++代码的测试,i = 0; i = i++
最后i的值是1,这点和Java不同。
作者:dantezhao |简书 | CSDN | GITHUB
文章地址:http://www.jianshu.com/p/580f52e2f38a
个人主页:http://www.jianshu.com/u/2453cf172ab4
文章可以转载, 但必须以超链接形式标明文章原始出处和作者信息
网友评论