Java中的缓存变量机制
今天刷到一个有意思的题目,如下:
package algorithms.com.guan.javajicu;
public class Inc {
public static void main(String[] args) {
Inc inc = new Inc();
int i = 0;
inc.fermin(i);
i= i ++;
System.out.println(i);
}
void fermin(int i){
i++;
}
}
一开始我以为输出结果是[2],但是结果却大跌眼镜,输出结果为[0]。
这道题中存在两个陷阱,
第一个比较复杂,涉及到两个知识点
- 值传递和引用传递;
- 局部变量的作用域。
首先,先复习一下值传递和引用传递的区别吧。
java中存在8中基本数据类型。当函数的参数类型为基本数据类型时,调用函数的时候,java会将实际参数复制一份传递到函数中。在函数中对于该值进行操作,不会对实际参数产生影响。这就是值传递。
引用传递是当函数的参数类型不是基本数据类型时,调用函数的时候,会将实际参数的引用地址传到函数中。这样在函数中对于该引用进行操作,就会改变引用地址对应的值。
上题中的【inc.fermin(i); 】就是值传递,在fermin()方法中对于i操作不会影响到main函数中的i。
然后就是局部变量作用域的问题。
函数fermin()中的i为局部变量,它和main函数中的i为两个不同的变量。且局部变量i只在函数fermin()中产生作用。当fermin()执行完毕时,局部变量i的值为1。但是当main函数执行到第7行时,局部变量i就不在产生作用,会被回收。
所以当上题第6行执行完,第7行还没有执行的时候,main函数的i依旧等于0。
第二个陷阱就是缓存变量机制
i = i++在java中实际上等价于以下三句代码:
int temp = i;
i = i + 1;
i = temp;
上述解释很直接,但是应该有人不能理解为什么会有这样的等价?
Java虚拟机栈(JVM Stack)描述的是Java方法执行的内存模型,而JVM内存模型是基于“栈帧”的,每个栈帧中都有 局部变量表 和 操作数栈 (还有动态链接、return address等),那么JVM是如何执行这个语句的呢?通过javap大致可以将上面的两行代码翻译成如下的JVM指令执行代码。 0: iconst_0 1: istore_1 2: iload_1 3: iinc 1, 1 6: istore_1 7: iload_1 接下来分析一下JVM是如何执行的: 第0:将int类型的0入栈,就是放到操作数栈的栈顶 第1:将操作数栈栈顶的值0弹出,保存到局部变量表 index (索引)值为1的位置。(局部变量表也是从0开始的,0位置一般保存当前实例的this引用,当然静态方法例外,因为静态方法是类方法而不是实例方法) 第2:将局部变量表index 1位置的值的副本入栈。(这时局部变量表index为1的值是0,操作数栈顶的值也是0) 第3:iinc是对int类型的值进行自增操作,后面第一个数值1表示,局部变量表的index值,说明要对此值执行iinc操作,第二个数值1表示要增加的数值。(这时局部变量表index为1的值因为执行了自增操作变为1了,但是操作数栈中栈顶的值仍然是0) 第6:将操作数栈顶的值弹出(值0),放到局部变量表index为1的位置(旧值:1,新值:0),覆盖了上一步局部变量表的计算结果。 第7:将局部变量表index 1位置的值的副本入栈。(这时局部变量表index为1的值是0,操作数栈顶的值也是0)。
总结:从执行顺序可以看到,这里第1和第6执行了2次将0赋值给变量i的操作(=号赋值),i++操作是在这两次操作之间执行的,自增操作是对局部变量表中的值进行自增,而栈顶的值没有发生变化,这里需要注意的是保存这个初始值的地方是操作数栈而不是局部变量表,最后再将栈顶的值覆盖到局部变量表i所在的索引位置中去。
好了,不管看没看懂,但是最开始的直接解释应该没有人没看懂,下课!