学习 i++ 和 ++i 的本质区别

最近在朋友圈看到一篇解释 i++++i 的文章,觉得非常有意思,就把要点记录下来。

你的解释不是我想要的…

先看两段代码 HelloWorld_1HelloWorld_2

1
2
3
4
5
6
7
8
9
// HelloWorld_1.java
public class HelloWorld_1 {
public static void main(String[] args) {
int i = 0;
i = i++;
System.out.println(i); // 0
}
}

1
2
3
4
5
6
7
8
// HelloWorld_2.java
public class HelloWorld_2 {
public static void main(String[] args) {
int i = 0;
i = ++i;
System.out.println(i); // 1
}
}

大家应该一眼就知道运行结果,第一段代码是 0,第二段代码是 1,但关键是:为什么只是换了一下顺序,结果就不一样了?

一般的解释:i = i++,会先赋值,再加一,所以结果是 0,而 i = ++i,会先把 i 加一,然后再赋值,所以结果是 1。

全场感叹,都向那位同学投以敬佩的目光,毕竟他的理论足以解释现象。

然而,提问的人说了一句,“你的解释不是我想要的……”

一般的解释不能说不对,但是没有说到关键点上,因为这是以高级语言的角度来解释的。为了深入理解,我们需要从更低级的层面去获取答案。

Java 的跨平台靠 JVM(Java 虚拟机)这个翻译官来实现。你写好的代码,会被编译成一个 .class 文件,也就是 Java 字节码文件,这里面记录的是一系列要在 JVM 执行的指令。接着,你拿着这份字节码指令,去到任意一个 JVM(Linux 的 JVM、OS X 的 JVM),它们都会帮你把它翻译成不同平台所对应的机器指令(机器码)。这就实现了跨平台。

回到问题,i++++i 为什么会不一样呢?我们先通过 javacjavap 命令获得字节码指令。

1
javac HelloWorld_1.java

可以得到 HelloWorld_1.class 文件。class 文件里面都是二进制的数据,因为这些都是告诉 JVM 要做什么事情的指令,而机器只看得懂二进制。所以,我们需要对这个二进制反汇编,把它变成人类看得懂的语言。

1
javap -c HelloWorld_1.class

注意:为了方便比较,源代码中的 System.out.println(i) 已被删掉。

命令执行后,控制台打印出一系列的字节码指令,其中main函数的字节码指令如下:

1
2
3
4
5
6
7
8
public static void main(java.lang.String[]);
Code:
0: iconst_0
1: istore_1
2: iload_1
3: iinc 1, 1
6: istore_1
7: return

这一串的指令,主要涉及到两个数据结构,一个是操作数栈(operand stack),另一个是局部变量表(local variable)。前者是栈,后者是数组。

  1. iconst_0

    把一个值为 0 的 int 值压到操作数栈中。

  2. istore_1

    从操作数栈中弹出一个值,存放到局部变量表下标为 1 的位置(为什么不是 0 或者 2 ?因为前面有个 String 数组的参数 args,所以不是 0。又因为方法是 static 的,没有 this 引用,所以不是 2)。

    以上两条指令对应的是第一行代码 int i = 0,它实现了给 i 赋值,并且把 i 放到局部变量表的功能。接下来是 i = i++ 对应的指令。

  3. iload_1

    把局部变量表中索引为 1 的值压到操作数栈中(表中的值仍存在)。

  4. iinc 1, 1

    对局部变量表索引为 1 的值进行加一操作。iinc 指令第一个参数代表索引值,第二个参数代表加多少。

    此时,局部变量表中 i 的值等于 1,为什么最后打印出来还是 0 呢?问题出在最后一条指令。

  5. istore_1

    从操作数栈中弹出一个值,将它赋值给局部变量表中索引为 1 的值。

    完蛋,这下 i 又变成 0 了!

这就是 i 为 0 的本质原因。向别人解释的时候可以总结为以下三点:

  1. 赋值的本质是两步操作,先将等号右边的值压进操作数栈(iload),再将操作数栈中的值弹出至局部变量表中对应的位置上(istore)。
  2. ++ 的本质是 iinc 指令,操作的对象局部变量表,而不是操作数栈。
  3. i = i++ 对应三条指令的顺序是:iload_1、iinc 1,1、istore_1。

至于 i = ++i,它与前者的区别在 iload_1 和 iinc 的顺序上。当 iinc 在前,赋值前局部变量表中对应的值已为 1,且赋值操作的两条指令相邻,不存在归零现象。

最后,本文所提到的操作数栈和局部变量表只是 JVM 运行时数据区域中很小的一块(JVM Stack, 虚拟机栈),完整的模型图如下:

JVM 运行时数据区域图

参考:Java 第一课 by Bridge4You