美文网首页程序员
详解Java的i++问题

详解Java的i++问题

作者: 2453cf172ab4 | 来源:发表于2017-04-04 18:59 被阅读241次

    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);
    

    结果

    先由已知的知识来推测执行过程:

    1. 先将i赋值为0,然后将该slot值推入栈顶,栈顶值为0
    2. i进行两次自加,此时slot值为2,推入栈顶,栈顶值为2
    3. 栈顶前两个int相加,结果存入栈顶,栈顶值为2
    4. 再将slot值推入栈顶,栈顶值为2
    5. 注意:slot的值自增1,但是没有推入栈顶。
    6. 栈顶前两个int相加,结果存入栈顶,栈顶值为4
    7. 结果是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);
    

    结果

    再次根据已知的知识来推测执行过程:

    1. 先将i赋值为0,i自增1,然后推入栈顶,此时slot值为1,栈顶值为1
    2. 将slot值继续推入栈顶,此时slot值为1,栈顶值为1
    3. 栈顶前两个int相加,结果存入栈顶,此时slot值为1,栈顶值为2
    4. i指向的slot值自增1,此时slot值为2,栈顶值为2
    5. i指向的slot值自增1,此时slot值为3,栈顶值为2
    6. 将slot值推入栈顶,并且栈顶前两个int相加,结果存入栈顶,此时slot值为3,栈顶值为5
    7. 最后结果即为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
    文章可以转载, 但必须以超链接形式标明文章原始出处和作者信息

    微信公众号

    相关文章

      网友评论

        本文标题:详解Java的i++问题

        本文链接:https://www.haomeiwen.com/subject/cjdzottx.html