专栏首页Java那些事i++和++i的联系与区别!超详细原理分析!

i++和++i的联系与区别!超详细原理分析!

1. i++和++i的基本概念

在几乎所有的命令式编程语言中,必然都会有 i++ 和 ++i 这种语法。有些语言中 i++ 和 ++i 既可以作为左值又可以作为右值,笔者专门测试了一下,在 Java 语言中,这两条语句都只能作为右值,而不能作为左值。同时,它们都可以作为独立的一条指令执行。

关于 i++ 和 ++i 的区别,稍微有经验的程序员都或多或少都是了解的,为了文章的完整性,本文也通过实例来简单地解释一下。

案例 1:

/**
 * @author pcwl
 * @description i++ 和 ++i 详解
 */
public class Test {

    public static void main(String[] args) {
        int i= 1;
        int j = i++;
        System.out.println("j = " + j);
        System.out.println("i = " + i);
    }
}

运行结果:

案例 2:

public class Test {

    public static void main(String[] args) {
        int i= 1;
        int j = ++i;
        System.out.println("j = " + j);
        System.out.println("i = " + i);
    }
}

运行结果:

上面的例子中可以看到,无论是 i++ 和 ++i 指令,对于 i 变量本身来说是没有任何区别的,指令执行的结果都是 i 变量的值加 1。而对于 j 来说前 ++ 和后 ++ 结果却不一样了。

int i = 1;
int j = i++; // 先将i的原始值(1)赋值给变量j(1),然后i变量的值加1
int j = ++i; // 先将i变量的值加1,然后将i的当前值(2)赋值给变量j(2)

2. i++ 和 ++i 的实现原理

接下来让我们深入到编译后的字节码层面上来了解 i++ 和 ++i 的实现原理,为了方便对比,将这两个指令分别放在 2 个不同的方法中执行,源代码如下:

/**
 * @author pcwl
 * @description i++ 和 ++i 详解
 */
public class Test {

    public void testIPlus() {
        int i = 0;
        int j = i++;
    }

    public void testPlusI() {
        int i = 0;
        int j = ++i;
    }
}

将上面的源代码编译之后,使用 javap 命令查看编译生成的代码(忽略次要代码)如下:

...
{
  ...

  public void testIPlus();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=3, args_size=1
         0: iconst_0               // 生成整数0
         1: istore_1               // 将整数0赋值给1号存储单元(即变量i)
         2: iload_1                // 将1号存储单元的值加载到数据栈(此时 i=0,栈顶值为0)
         3: iinc          1, 1     // 1号存储单元的值+1(此时 i=1)
         6: istore_2               // 将数据栈顶的值(0)取出来赋值给2号存储单元(即变量j,此时i=1,j=0)
         7: return                 // 返回时:i=1,j=0
      LineNumberTable:
        line 4: 0
        line 5: 2
        line 6: 7

  public void testPlusI();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=3, args_size=1
         0: iconst_0                // 生成整数0
         1: istore_1                // 将整数0赋值给1号存储单元(即变量i)
         2: iinc          1, 1      // 1号存储单元的值+1(此时 i=1)
         5: iload_1                 // 将1号存储单元的值加载到数据栈(此时 i=1,栈顶值为1)
         6: istore_2                // 将数据栈顶的值(1)取出来赋值给2号存储单元(即变量j,此时i=1,j=1)
         7: return                  // 返回时:i=1,j=1
      LineNumberTable:
        line 9: 0
        line 10: 2
        line 11: 7
}
...

可以从上面的字节码文件看出,造成结果不同的原因就是:“1 号存储单元的值加 1 的操作”和“将 1 号存储单元的值加载到数据栈”的先后顺序造成的。如果前者在后者之前,则结果就是 1,反之则为 0。

3. i++ 和 ++i 使用的一些坑

i++ 和 ++i 在一些特殊场景下可能会产生意想不到的结果,本节介绍两种会导致结果混乱的使用场景,并剖析其原因。

01

案例1:i=i++导致的结果异常

public class Test {
    public static void main(String[] args) {
        int i = 0;
        i = i++;
        System.out.println("i = " + i);   // 0
    }
}

运行结果:

正常来讲,执行的结果应该是:i = 1,实际结果却是:i = 0,这多少会让人有些诧异。为什么会出现这种情况呢?我们来从编码后的代码中找答案。上面的代码编译后的核心代码如下:

0: iconst_0                   // 生成整数0
1: istore_1                   // 将整数0赋值给1号存储单元(即变量i,i=0)
2: iload_1                    // 将1号存储单元的值加载到数据栈(此时 i=0,栈顶值为0)
3: iinc          1, 1         // 1号存储单元的值+1(此时 i=1)
6: istore_1                   // 将数据栈顶的值(0)取出来赋值给1号存储单元(即变量i,此时i=0)
7: getstatic      #16         // 下面是打印到控制台指令
10: new           #22              
13: dup
14: ldc           #24                
16: invokespecial #26                
19: iload_1
20: invokevirtual #29               
23: invokevirtual #33                
26: invokevirtual #37                
29: return

从编码指令可以看出,i 被栈顶值所覆盖,导致最终 i 的值仍然是 i 的初始值。无论重复多少次 i = i++ 操作,最终 i 的值都是其初始值。

实际上:i++ 有中间缓存变量,,i = i++ 等价于:

temp = i;
i = i + 1;
i = temp;

所以 i 不变, 依然是0。

02

案例2

public class Test {
    public static void main(String[] args) {
        int i = 0;
        i++;
        System.out.println("i = " + i);   // 1
    }
}

运行结果:

和上面上面的两端代码中唯一的差别就是 i++ 的结果有没有赋值给 i ,但是输出的 i 的结果一个加了1,而1个没有加。这是为什么呢?我们看下编译的字节码文件:

0: iconst_0              // 生成整数0                
1: istore_1              // 将整数0赋值给1号存储单元(即变量i,i=0)
2: iinc          1, 1    // 1号存储单元的值+1(此时 i=1)
5: getstatic     #2                  
8: new           #3                  
11: dup
12: invokespecial #4                  
15: ldc           #5                  
17: invokevirtual #6                  
20: iload_1
21: invokevirtual #7                  
24: invokevirtual #8                  
27: invokevirtual #9                  
30: return

本文分享自微信公众号 - 程序员乔戈里(CXYqiaogeli),作者:鹏程万里

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-11-23

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 牛逼!一行代码居然能解决这么多曾经困扰我半天的算法题

    春节假期这么长,干啥最好?当然是折腾一些算法题了,下面给大家讲几道一行代码就能解决的算法题,当然,我相信这些算法题你都做过,不过就算做过,也是可以看一看滴,毕竟...

    乔戈里
  • 面试官,求求你不要问我这么简单但又刁难的算法题了

    有时候面试官往往会问我们一些简单,但又刁难的问题,主要是看看你对问题的处理思路。如果你没接触过这些问题,可能一时之间还真不知道怎么处理才比较好,这种题更重要的是...

    乔戈里
  • 一文带你了解被 BATJ 问烂的 TopK 问题

    Leetcode : 215. Kth Largest Element in an Array

    乔戈里
  • Data Structure_数组_栈_队列_链表_霍夫曼数组栈队列链表哈夫曼

    这就表示一个数组,这个数组有八个元素存放。对于元素的获取,主要就是通过下标获取,所以索引对于数组是很重要的,这个索引可以是有意义的,也可以是没有意义的。比如ar...

    西红柿炒鸡蛋
  • Data Structure_数组_栈_队列_链表_霍夫曼

    这就表示一个数组,这个数组有八个元素存放。对于元素的获取,主要就是通过下标获取,所以索引对于数组是很重要的,这个索引可以是有意义的,也可以是没有意义的。比如ar...

    西红柿炒鸡蛋
  • MyBatis 基本构成与框架搭建

            根据配置信息(eg:mybatis-config.xml)或者代码来生成SqlSessionFactory。

    Rekent
  • RecyclerView零点突破(动画+边线篇)

    张风捷特烈
  • [C#2] 4-可空类型、静态类

    1. 可空类型 值类型是不可以为null的[即不可为空值], 假如我们想让它为null呢[比如它对映这数据库中的某个表的某个字段,但是这个字段是null]。 自...

    blackheart
  • 搞定数据结构-栈和队列

    如下,使用栈结构操作. “网”这个错别字在栈顶,“网”改成”望”只需要将“网”从栈顶移除重新写入”望”.

    用户3045442
  • MVVM架构之自动增删改的极简RecycleView的实现

    介绍图 先上个源代码的链接:https://github.com/whenSunSet/MVVMRecycleView RecycleView是Google替...

    何时夕

扫码关注云+社区

领取腾讯云代金券