背景

i++ 还是 ++i 已经是 JVM 中很经典的问题了,今天索性来仔细研究一下。

使用 IDEA 配合 jclasslib 插件,将编译好的字节码反编译看看不同 ++ 之间的区别。

实验

情况一:普通自增。

1
2
int i = 10;
i++;
1
2
int i = 10;
++i;
1
2
3
4
0 bipush 10   // 将10压入操作数栈
2 istore_1 // 将刚才压入的10弹出至局部变量表的位置1
3 iinc 1 by 1 // 将局部变量表位置1的变量加1
6 return

经过测试,不论是 i++ 还是 ++i,编译出的字节码都是一样的,在这种普通的自增用法中,写成左自增还是右自增没有区别,而且都是只用执行一条字节码指令。

情况二:自增时赋值。

1
2
int i = 10;
int j = i++;
1
2
3
4
5
6
0 bipush 10   // 将10压入操作数栈
2 istore_1 // 将刚才压入的10弹出至局部变量表的位置1
3 iload_1 // 将局部变量表位置1的变量取到操作数栈(局部变量表中变量值还在)
4 iinc 1 by 1 // 将局部变量表位置1的变量加1
7 istore_2 // 将栈顶的数弹出至局部变量表的位置2
8 return

可以看到,第0、2、4条指令其实还是情况一普通自增的指令,但是将 i++ 赋值给 j 这一步其实是从局部变量表中取出 i 的值,然后通过操作数栈直接存回局部变量表 j 的位置的。而自增的操作,并没有在操作数栈中进行,而是直接对局部变量表中的 i 执行的。因此,最终 j 的值为 10。

1
2
int i = 10;
int j = ++i;
1
2
3
4
5
6
0 bipush 10
2 istore_1
3 iinc 1 by 1
6 iload_1
7 istore_2
8 return

i++ 改写为 ++i 后,iload_1iinc 1 by 1 两条指令的执行顺序对调了。这就导致了在 i 存入局部变量表后,我们首先将 i 的值增加 1,然后再取出到操作数栈。这样对 j 赋值时,取到的 i 已经是自增后的 i 了,因此 j 的值为 11。

情况三:自增后赋给自己。

1
2
int i = 10;
i = i++;
1
2
3
4
5
6
0 bipush 10
2 istore_1
3 iload_1
4 iinc 1 by 1
7 istore_1
8 return
1
2
int i = 10;
i = ++i;
1
2
3
4
5
6
0 bipush 10
2 istore_1
3 iinc 1 by 1
6 iload_1
7 istore_1
8 return

情况三和情况二类似,只不过原来从操作数栈中弹出至局部变量表位置 2 的指令,现在因为要赋值给自己,于是变成了 istore_1。因此,和情况二一致,前一种里 i 的最终值为 10,后一种里 i 的最终值为 11。

情况四:加号噩梦。

1
2
int i = 10;
int j = i++ + ++i;
1
2
3
4
5
6
7
8
9
 0 bipush 10
2 istore_1
3 iload_1
4 iinc 1 by 1
7 iinc 1 by 1
10 iload_1
11 iadd
12 istore_2
13 return

在这段代码下,前两条指令是普通地将 10 通过操作数栈存入局部变量表位置 1。然后接下来把刚才存入的 10 取到操作数栈,此时操作数栈只有取出的这个 10。

接下来第 4 和第 7 条指令,将局部变量表中的 i 分别加 1。经过这两条命令的操作,此时局部变量表位置 1 存放的 i 已经变成了 12,但操作数栈中的数仍为 10 不变。

接下来再将局部变量表中的 i 取到操作数栈,此时操作数栈中有两个数,分别是刚开始取出来的 10,和刚取出来的 12。

接下来第 11 条命令对操作数栈中的两个数执行求和操作,这个命令会使 12 和 10 依次出栈,求和变为 22 后压回栈中。第 12 条命令将栈中仅剩的操作数 12 弹出,存放至局部变量表的位置 2。

经过以上操作,局部变量表中最终位置 1 的 i 值为 12,位置 2 的 j 的值为 22。

让我们再来个更恐怖的:

1
2
int i = 10;
int j = i + ++i * i++;
1
2
3
4
5
6
7
8
9
10
11
 0 bipush 10   // 局部变量表:空      操作数栈:10
2 istore_1 // 局部变量表:10 操作数栈:空
3 iload_1 // 局部变量表:10 操作数栈:10
4 iinc 1 by 1 // 局部变量表:11 操作数栈:10
7 iload_1 // 局部变量表:11 操作数栈:10, 11
8 iload_1 // 局部变量表:11 操作数栈:10, 11, 11
9 iinc 1 by 1 // 局部变量表:12 操作数栈:10, 11, 11
12 imul // 局部变量表:12 操作数栈:10, 121
13 iadd // 局部变量表:12 操作数栈:131
14 istore_2 // 局部变量表:12, 131 操作数栈:空
15 return

在这个例子中我们可以看到,四则运算的过程有点类似后缀表达式在栈中的运算过程,是从左到右依次进栈,在入栈的同时按运算优先级计算后出栈。其中,i 不加自增修饰的话,值就是 i 入栈时的值。如果是 i++ 的话,自增前的值入栈,然后局部变量表对应的值自增。如果是 ++i 的话,则先将局部变量表自增,然后将自增后的值压入操作数栈。

一般结论

如果写代码时只是为了将变量的值加一,而不涉及同一条语句中的运算、赋值时,用 i++++i 其实是没区别的。

如果涉及了变量的四则运算,则按照逆波兰表达式的顺序,将操作数依次入栈。入栈时,只有 ++i 是先更新局部变量表,再将更新后的值入栈。i 和 i++ 在计算时都是将原值压入栈。