前言
基础不牢,地动山摇。”对于一个刚入门的程序员来说,扎实的基础是是保持自身竞争力的坚石。记得刚刚学会看代码的时候总是被i++和++i弄蒙,我想对于很多小白一定和我有相同的感受。为了夯实自己的基础,同时也希望能帮助到别人,所以写下了这篇文章
请看下面的代码自增变量之i++
![80f4a3425b9950c0b124bb9776d256e8.png](https://i-blog.csdnimg.cn/blog_migrate/c8635da7b5da3a81566127959fae6484.jpeg)
你知道最后i的值等于多少吗?
从上面的代码中,第一眼就认为肯定是输出 1,我们一般都会认为它的步骤首先是定义一个i变量并且初始值为0,第二步i++是一个自增行为此时i=1,第三步把i的值1赋值给i,此时i=1?
我们运行一下看看结果
![cdcefd31f2e81e808bc96f02de89d0d4.png](https://i-blog.csdnimg.cn/blog_migrate/bdbb7630ff34f03830abd3efe6a43110.jpeg)
i的值打印出来为0,这和我们想的可不一样。这是怎么回事呢?
其实如果你理解JVM的内存模型,就不难理解为什么答案i返回的是0,而不是1。
i = i++;
里面有两个最关键的部分,i++和 i=i,先是i自增,然后重新为i赋值 i=i
先看i++
java编译器在遇到i++的时候,会重新为变量运算分配一块内存空间,存放原始的值,在完成运算后,再将内存空间释放掉
i = i++,会重新开辟空间存放 i 的值,在原始位置保存i+1, 之后在执行赋值操作,将i+1的值覆盖掉。
从内存出发看看运行原理
Java虚拟机栈(JVM Stack)描述的是Java方法执行的内存模型,而JVM内存模型是基于“栈帧”的,每个栈帧中都有 局部变量表 和 操作数栈 (还有动态链接、return address等),那么JVM是如何执行这个语句的呢?通过javap大致可以将上面的两行代码翻译成如下的JVM指令执行代码。
利用javap反编译出JVM的字节码指令
![6c1401c723e66510c77ece1f3ee27b6e.png](https://i-blog.csdnimg.cn/blog_migrate/eb725f01d892a9242b5e9c854b52b8ce.jpeg)
Code:
0: iconst_0
1: istore_1
2: iload_1
3: iinc 1, 1
6: istore_1
7: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream;
10: iload_1
11: invokevirtual #22 // Method java/io/PrintStream.println:u
14 return
JVM调用执行该方法时,对应栈帧(Stack Frame) 在虚拟机中入栈;栈帧中包含:局部变量表、操作数栈、动态链接、方法返回地址等信息。
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量,i就放在局部变量表中;而操作数栈是一个后入先出的栈,用于方法的计算。
- 0: iconst_0:(生成整数0)将int类型的0入栈,就是放到操作数栈的栈顶
- 1: istore_1:(将整数0赋值给1号存储单元)将操作数栈栈顶的值0弹出,保存到局部变量表 index (索引)值为1的位置。(局部变量表也是从0开始的,0位置一般保存当前实例的this引用,当然静态方法例外,因为静态方法是类方法而不是实例方法)
- 2: iload_1:(将1号存储单元的值加载到数据栈(此时 i=0,栈顶值为0))将局部变量表index 1位置的值的副本入栈。(这时局部变量表index为1的值是0,操作数栈顶的值也是0)
- 3: iinc 1, 1:(1号存储单元的值+1(此时 i=1))iinc是对int类型的值进行自增操作,后面第一个数值1表示,局部变量表的index值,说明要对此值执行iinc操作,第二个数值1表示要增加的数值。(这时局部变量表index为1的值因为执行了自增操作变为1了,但是操作数栈中栈顶的值仍然是0)
- 6: istore_1:(将数据栈顶的值(0)取出来赋值给1号存储单元(即变量i,此时i=0))将操作数栈顶的值弹出(值0),放到局部变量表index为1的位置(旧值:1,新值:0),覆盖了上一步局部变量表的计算结果。
- 10: iload_1:将局部变量表index 1位置的值的副本入栈。(这时局部变量表index为1的值是0,操作数栈顶的值也是0)
从编码指令可以看出,i被栈顶值所覆盖,导致最终i的值仍然是i的初始值。无论重复多少次i = i++操作,最终i的值都是其初始值。
这个问题其实想明白了就很简单,只需要理解两个关键的地方:1. 表达式的返回值。 2. 临时变量。
i = i ++;
此语句包含两个操作:自加操作和赋值操作
赋值操作被编译成两条命令,即iload_?(取值,?代表取哪里的值,即右值)和istore_?(存值,即左值,存到声明时分配的存储单元)
自加操作直接在储存单元中,进行自加。
所以i = i ++ ;就可以解释为:
先将i的值取出放到数据栈(iload_1),然后i发生自加,此时1号存储单元值变为1;最后将数据栈中的值 0 弹出,赋给i(istore_1),此时1号存储单元原来的值被 0 覆盖。
此处还有一个问题就是
iinc 1,1和istore_1的先后问题:
原理不难解释:
赋值操作应该在右表达式完成后才可以赋值,
总结
i++利用了中间缓存变量
i++ 有中间缓存变量, i = i++ 等价于
temp = i;
i = i + 1;
i = temp;
++号在后面的意思是先赋值然后自身加1,i本来就等于0,i++就是在局部
从jvm执行顺序可以看到,这里第1和第6执行了2次将0赋值给变量i的操作(=号赋值),i++操作是在这两次操作之间执行的,自增操作是对局部变量表中的值进行自增,而栈顶的值没有发生变化,这里需要注意的是保存这个初始值的地方是操作数栈而不是局部变量表,最后再将栈顶的值覆盖到局部变量表i所在的索引位置中去。
![9a3c59ecb405fa24663b39aa5a4797b3.png](https://i-blog.csdnimg.cn/blog_migrate/15723306e3f24341481298677618c7f8.jpeg)
现在你知道I++了,如果 我们改为++i呢?最后i输出几?