为什么这会陷入无限循环?
我有以下代码:
public class Tests { public static void main(String[] args) throws Exception { int x = 0; while(x<3) { x = x++; System.out.println(x); } } }
我们知道他应该只写x++
或者x=x+1
,但是在x = x++
它应该首先把x
赋给自己,然后增加它。 为什么x
继续0
作为价值?
–update
这是字节码:
public class Tests extends java.lang.Object{ public Tests(); Code: 0: aload_0 1: invokespecial #1; //Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]) throws java.lang.Exception; Code: 0: iconst_0 1: istore_1 2: iload_1 3: iconst_3 4: if_icmpge 22 7: iload_1 8: iinc 1, 1 11: istore_1 12: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream; 15: iload_1 16: invokevirtual #3; //Method java/io/PrintStream.println:(I)V 19: goto 2 22: return }
我会阅读有关说明 ,试图了解…
注意 :最初我在这个答案中发布了C#代码,目的是为了说明,因为C#允许您通过引用传递int
参数与ref
关键字。 我决定使用我在Google上find的第一个MutableInt
类来实际更新合法的Java代码,以便大致了解在C#中ref
作用。 我真的不知道这是否有助于或伤害了答案。 我会说,我个人并没有做太多的Java开发; 所以我知道可以有更多的习惯用法来说明这一点。
也许如果我们写出一个方法来做相当于x++
操作,它会使这个更清晰。
public MutableInt postIncrement(MutableInt x) { int valueBeforeIncrement = x.intValue(); x.add(1); return new MutableInt(valueBeforeIncrement); }
对? 递增传递的值并返回原始值:这是postincrement操作符的定义。
现在,让我们来看看这个行为在你的示例代码中的performance如何:
MutableInt x = new MutableInt(); x = postIncrement(x);
postIncrement(x)
做什么? 增加x
,是的。 然后返回增量之前的x
。 这个返回值然后被分配给x
。
所以赋值给x
的值的顺序是0,然后是1,然后是0。
如果我们重写上面的话,这可能会更清楚:
MutableInt x = new MutableInt(); // x is 0. MutableInt temp = postIncrement(x); // Now x is 1, and temp is 0. x = temp; // Now x is 0 again.
你注意到当你用x
代替上面赋值左边的y
,“你可以看到它先增加x,然后把它归类为y”,这让我感到困惑。 这不是x
分配给y
; 它是以前赋值给x
的值 。 真的,注入y
与上面的场景没有什么不同。 我们只是得到:
MutableInt x = new MutableInt(); // x is 0. MutableInt y = new MutableInt(); // y is 0. MutableInt temp = postIncrement(x); // Now x is 1, and temp is 0. y = temp; // y is still 0.
所以很明显: x = x++
实际上不会改变x的值。 它总是使x有值x 0 ,然后x 0 + 1,然后再x 0 。
更新 :顺便说一句,为了避免你怀疑x
在上面的例子中被赋值为1之间的增量操作和赋值,我把一个快速演示放在一起来说明这个中间值确实是“存在的”,尽pipe它将永远不会在执行线程上“看到”。
演示程序调用x = x++;
在一个循环中,一个单独的线程连续地将x
的值打印到控制台。
public class Main { public static volatile int x = 0; public static void main(String[] args) { LoopingThread t = new LoopingThread(); System.out.println("Starting background thread..."); t.start(); while (true) { x = x++; } } } class LoopingThread extends Thread { public @Override void run() { while (true) { System.out.println(Main.x); } } }
以下是上述程序输出的摘录。 注意1和0的不规则出现。
开始后台线程... 0 0 1 1 0 0 0 0 0 0 0 0 0 0 1 0 1
x = x++
工作方式如下:
- 首先它评估expression式
x++
。 对这个expression式的评估产生一个expression式值(这是增量前x
的值)和增量x
。 - 之后它将expression式值赋给
x
,覆盖增加后的值。
所以,事件序列如下所示(这是一个真正的反编译的字节码,由javap -c
,带有我的注释):
8:iload_1 //记住堆栈中x的当前值 9:iinc 1,1 //增加x(不改变堆栈) 12:istore_1 //将堆栈中的重新编排的值写入x
为了比较, x = ++x
:
8:iinc 1,1 //增加x 11:iload_1 //将x的值压入堆栈 12:istore_1 //将值从栈中popup到x
发生这种情况是因为x
的值根本没有增加。
x = x++;
相当于
int temp = x; x++; x = temp;
说明:
我们来看看这个操作的字节码。 考虑一个样本类:
class test { public static void main(String[] args) { int i=0; i=i++; } }
现在运行这个类的反汇编器,我们得到:
$ javap -c test Compiled from "test.java" class test extends java.lang.Object{ test(); Code: 0: aload_0 1: invokespecial #1; //Method java/lang/Object."<init>":()V 4: return 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 }
现在Java VM是基于堆栈的,这意味着对于每个操作,数据将被压入堆栈,并且从堆栈中popup数据以执行操作。 还有另一个数据结构,通常是一个数组来存储局部variables。 给出的局部variablesID只是数组的索引。
让我们看一下main()
方法中的助记符 :
-
iconst_0
:常量值0
被压入堆栈。 -
istore_1
:堆栈的顶层元素被popup并存储在索引为1
的局部variables中
这是x
。 -
iload_1
:位置1
处的x
值为0
值被压入堆栈。 -
iinc 1, 1
:存储器位置1
值增加1
。 所以x
现在变成1
。 -
istore_1
:堆栈顶部的值被存储到内存位置1
。 这是0
分配给x
覆盖其递增的值。
因此, x
的值不会改变,导致无限循环。
- 前缀符号会在计算expression式之前递增variables。
- 后缀表示法会在expression式评估之后递增。
但是“ =
”具有比“ ++
”更低的运算符优先级。
所以x=x++;
应该评估如下
-
x
准备转让(评估) -
x
递增 - 先前分配给
x
的x
值。
没有什么地方的答案,所以在这里:
当你写int x = x++
,你不会把x
指定为新值,而是将x
赋值为x++
expression式的返回值。 正如Colin Cochrane的回答所暗示的那样,恰好是x
的原始价值。
为了好玩,testing下面的代码:
public class Autoincrement { public static void main(String[] args) { int x = 0; System.out.println(x++); System.out.println(x); } }
结果将是
0 1
expression式的返回值是x
的初始值,它是零。 但稍后,在读取x
的值时,我们会收到更新的值,即一个值。
已经被其他人解释清楚了。 我只是包含相关Java规范部分的链接。
x = x ++是一个expression式。 Java将遵循评估顺序 。 它将首先评估expression式x ++, 它将递增x并将结果值设置为之前的x值 。 然后它将expression式结果赋给variablesx。 最后,x回到先前的值。
这个说法:
x = x++;
评估如下:
- 将
x
推入堆栈; - 增加
x
; - 从堆栈中popup
x
。
所以价值不变。 比较:
x = ++x;
其评估为:
- 增加
x
; - 将
x
推入堆栈; - 从堆栈中popup
x
。
你想要的是:
while (x < 3) { x++; System.out.println(x); }
答案非常简单。 它与评估事物的顺序有关。 x++
返回值x
然后增加x
。
因此,expression式x++
的值是0
。 所以你在循环中每次分配x=0
。 当然, x++
递增这个值,但是这在分配之前发生。
从http://download.oracle.com/javase/tutorial/java/nutsandbolts/op1.html
递增/递减运算符可以在操作数之前(前缀)或之后(后缀)应用。 代码结果++; 和++结果; 两者的结果都会增加一个。 唯一的区别是前缀版本(++结果)评估为递增值, 而后缀版本(结果++)评估为原始值 。 如果您只是执行简单的递增/递减操作,则select哪个版本并不重要。 但是如果你在更大的expression式中使用这个运算符,那么你select的运算符可能会有很大的不同。
为了说明,请尝试以下操作:
int x = 0; int y = 0; y = x++; System.out.println(x); System.out.println(y);
哪个会打印1和0。
您正在有效地获得以下行为。
- 把x的值(0)作为右边的“结果”
- 递增x的值(所以x现在是1)
- 将右侧的结果(保存为0)赋给x(x现在为0)
这个想法是,增量后运算符(x ++)递增该variables后,返回它的值用于在其中使用的公式。
编辑:添加一点点,因为评论。 考虑如下。
x = 1; // x == 1 x = x++ * 5; // First, the right hand side of the equation is evaluated. ==> x = 1 * 5; // x == 2 at this point, as it "gave" the equation its value of 1 // and then gets incremented by 1 to 2. ==> x = 5; // And then that RightHandSide value is assigned to // the LeftHandSide variable, leaving x with the value of 5.
您并不需要机器码来了解发生了什么事情。
根据定义:
-
赋值运算符评估右边expression式,并将其存储在一个临时variables中。
1.1。 x的当前值被复制到这个临时variables中
1.2。 x现在增加。
-
然后将临时variables复制到expression式的左侧,这是偶然的! 所以这就是为什么x的旧值再次被复制到自身。
这很简单。
这是因为在这种情况下它永远不会增加。 x++
会像使用这种情况一样在递增之前先使用它的值,如下所示:
x = 0;
但是,如果你做++x;
这会增加。
由于x++
的值为0,因此值保持为0。在这种情况下, x
的值是否增加并不重要,分配x=0
被执行。 这会覆盖x
的临时递增值(对于“非常短的时间”是1)。
这工作如何你期望另一个。 这是前缀和后缀之间的区别。
int x = 0; while (x < 3) x = (++x);
把x ++想象成一个函数调用,它返回增量之前的X(这就是为什么它被称为后增量)的原因。
所以操作顺序是:
1:在递增之前cachingx的值
2:增加x
3:返回caching的值(x在递增之前)
4:返回值分配给x
当++位于rhs时,结果会在数字递增之前返回。 更改为++ x,它会很好。 Java会优化这个来执行一个单一的操作(将x分配给x)而不是增量。
就我所知,由于赋值重写递增的值,所以发生错误与增量之前的值,也就是说,它撤消了增量。
具体而言,“x ++”expression式在增量之前具有“x”的值,而不是在增量后具有“x”的值的“++ x”。
如果你有兴趣研究字节码,我们将看看这三个问题:
7: iload_1 8: iinc 1, 1 11: istore_1
7:iload_1#将第二个局部variables的值放在堆栈上
8:iinc 1,1#将用1递增第二个局部variables,注意它保持堆栈不变!
9:istore_1#popup堆栈顶部,并将此元素的值保存到第二个局部variables
(您可以在这里阅读每个JVM指令的效果)
这就是为什么上面的代码会无限循环,而带有++ x的版本不会。 ++ x的字节码应该看起来完全不同,据我记得,从一年前我写的1.3 Java编译器中,字节码应该是这样的:
iinc 1,1 iload_1 istore_1
所以只需交换两条第一行,改变语义,以便在增量(即expression式的“值”)之后,堆栈顶部留下的值是增量之后的值。
x++ =: (x = x + 1) - 1
所以:
x = x++; => x = ((x = x + 1) - 1) => x = ((x + 1) - 1) => x = x; // Doesn't modify x!
而
++x =: x = x + 1
所以:
x = ++x; => x = (x = x + 1) => x = x + 1; // Increments x
当然,最终的结果和x++;
或++x;
在一条线上。
句子
x = x++;
“翻译”为
x = x; x = x + 1;
而已。
x = x++; (increment is overriden by = )
由于上面的陈述x从来没有达到3;
我想知道Java规范中是否有任何东西正确地定义了这个行为。 (这个陈述的含义显然是我懒得检查。)
注意Tom的字节码,关键字是7,8和11.第7行将x加载到计算堆栈中。 第8行增加x。 第11行将堆栈中的值存储回x。 在正常情况下,如果你不把值分配给自己,我不认为你有什么理由不能加载,存储,然后增加。 你会得到相同的结果。
比如,假设你有一个更正常的情况,你写了类似的东西:z =(x ++)+(y ++);
是否说(伪代码跳过技术性)
load x increment x add y increment y store x+y to z
要么
load x add y store x+y to z increment x increment y
应该是无关紧要的。 任何一个实施应该是有效的,我会想。
编写依赖于此行为的代码时,我会非常谨慎。 它看起来非常依赖于实现,在我看来是在规范之中。 唯一的区别是如果你做了一些疯狂的事情,比如这里的例子,或者如果你有两个线程运行,并且依赖于expression式中的评估顺序。
我认为,因为在Java ++中比=(赋值)具有更高的优先级…是吗? 看看http://www.cs.uwf.edu/~eelsheik/cop2253/resources/op_precedence.html …
如果你写x = x + 1 … +的优先级高于=(赋值)
x++
expression式计算为x
。 ++
部分会影响评估后的值 ,而不会影响语句 。 所以x = x++
被有效地转换成
int y = x; // evaluation x = x + 1; // increment part x = y; // assignment
在将值递增1之前,该值被分配给variables。
这是因为它后增加。 这意味着在评估expression式后variables会增加。
int x = 9; int y = x++;
x现在是10,但是y是9,x的值是递增的。
有关后期增量的定义,请参阅更多信息。
检查下面的代码,
int x=0; int temp=x++; System.out.println("temp = "+temp); x = temp; System.out.println("x = "+x);
输出将是,
temp = 0 x = 0
post increment
意味着增加值并在增量之前返回值 。 这就是为什么值的temp
是0
。 那么如果temp = i
,这又是一个循环(除了第一行代码)。 就像在这个问题!
递增运算符应用于您分配给的相同variables。 这是在惹麻烦 我相信你可以在运行这个程序的时候看到你的xvariables的值….这应该清楚为什么循环永远不会结束。