从 JVM 的视角看 i++ 和 ++i
背景
i++
还是 ++i
已经是 JVM 中很经典的问题了,今天索性来仔细研究一下。
使用 IDEA 配合 jclasslib 插件,将编译好的字节码反编译看看不同 ++
之间的区别。
实验
情况一:普通自增。
1 | int i = 10; |
1 | int i = 10; |
1 | 0 bipush 10 // 将10压入操作数栈 |
经过测试,不论是 i++
还是 ++i
,编译出的字节码都是一样的,在这种普通的自增用法中,写成左自增还是右自增没有区别,而且都是只用执行一条字节码指令。
情况二:自增时赋值。
1 | int i = 10; |
1 | 0 bipush 10 // 将10压入操作数栈 |
可以看到,第0、2、4条指令其实还是情况一普通自增的指令,但是将 i++
赋值给 j
这一步其实是从局部变量表中取出 i 的值,然后通过操作数栈直接存回局部变量表 j 的位置的。而自增的操作,并没有在操作数栈中进行,而是直接对局部变量表中的 i 执行的。因此,最终 j 的值为 10。
1 | int i = 10; |
1 | 0 bipush 10 |
将 i++
改写为 ++i
后,iload_1
和 iinc 1 by 1
两条指令的执行顺序对调了。这就导致了在 i 存入局部变量表后,我们首先将 i 的值增加 1,然后再取出到操作数栈。这样对 j 赋值时,取到的 i 已经是自增后的 i 了,因此 j 的值为 11。
情况三:自增后赋给自己。
1 | int i = 10; |
1 | 0 bipush 10 |
1 | int i = 10; |
1 | 0 bipush 10 |
情况三和情况二类似,只不过原来从操作数栈中弹出至局部变量表位置 2 的指令,现在因为要赋值给自己,于是变成了 istore_1
。因此,和情况二一致,前一种里 i 的最终值为 10,后一种里 i 的最终值为 11。
情况四:加号噩梦。
1 | int i = 10; |
1 | 0 bipush 10 |
在这段代码下,前两条指令是普通地将 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 | int i = 10; |
1 | 0 bipush 10 // 局部变量表:空 操作数栈:10 |
在这个例子中我们可以看到,四则运算的过程有点类似后缀表达式在栈中的运算过程,是从左到右依次进栈,在入栈的同时按运算优先级计算后出栈。其中,i 不加自增修饰的话,值就是 i 入栈时的值。如果是 i++
的话,自增前的值入栈,然后局部变量表对应的值自增。如果是 ++i
的话,则先将局部变量表自增,然后将自增后的值压入操作数栈。
一般结论
如果写代码时只是为了将变量的值加一,而不涉及同一条语句中的运算、赋值时,用 i++
和 ++i
其实是没区别的。
如果涉及了变量的四则运算,则按照逆波兰表达式的顺序,将操作数依次入栈。入栈时,只有 ++i
是先更新局部变量表,再将更新后的值入栈。i 和 i++
在计算时都是将原值压入栈。