为什么这会陷入无限循环?

我有以下代码:

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的值不会改变,导致无限循环。

  1. 前缀符号会在计算expression式之前递增variables。
  2. 后缀表示法会在expression式评估之后递增。

但是“ = ”具有比“ ++ ”更低的运算符优先级。

所以x=x++; 应该评估如下

  1. x准备转让(评估)
  2. x递增
  3. 先前分配给xx值。

没有什么地方的答案,所以在这里:

当你写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++; 

评估如下:

  1. x推入堆栈;
  2. 增加x ;
  3. 从堆栈中popupx

所以价值不变。 比较:

 x = ++x; 

其评估为:

  1. 增加x ;
  2. x推入堆栈;
  3. 从堆栈中popupx

你想要的是:

 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。

您正在有效地获得以下行为。

  1. 把x的值(0)作为右边的“结果”
  2. 递增x的值(所以x现在是1)
  3. 将右侧的结果(保存为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. 

您并不需要机器码来了解发生了什么事情。

根据定义:

  1. 赋值运算符评估右边expression式,并将其存储在一个临时variables中。

    1.1。 x的当前值被复制到这个临时variables中

    1.2。 x现在增加。

  2. 然后将临时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意味着增加值并在增量之前返回值 。 这就是为什么值的temp0 。 那么如果temp = i ,这又是一个循环(除了第一行代码)。 就像在这个问题!

递增运算符应用于您分配给的相同variables。 这是在惹麻烦 我相信你可以在运行这个程序的时候看到你的xvariables的值….这应该清楚为什么循环永远不会结束。