您的位置:首页 > 编程语言 > Java开发

Java:i++问题(详细分析)

2015-09-26 22:30 411 查看

i++问题引发的知识链

一个表面上简单的i++问题,让我从此带着敬畏之心去学习!

例子

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不同。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: