【问题标题】:Why does this go into an infinite loop?为什么会进入无限循环?
【发布时间】:2011-04-19 09:10:47
【问题描述】:

我有以下代码:

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 作为值?

--更新

这是字节码:

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

}

我会阅读instructions 以尝试理解...

【问题讨论】:

  • 我怀疑发生了什么: 1. 将 x 加载到寄存器中(=0); 2. 增加 x (x=1); 3. 将寄存器值存入 x (x=0)。在 C/C++ 中,这将是未定义的行为,因为没有正式的序列点来定义 2 和 3 的顺序。希望有人可以引用 Java 规范中的等效内容。
  • 我们在 C++ 中尝试过这个,看看会发生什么,它会打印 1,2,3 并退出。我原本没想到。我认为它依赖于编译器,因为它是未定义的行为。我们使用了 gnu g++。
  • @saj x++ 是后增量; x=result 的赋值; x++result 是原始的 x (并且有增量的副作用,但这不会改变结果),所以这可以解释为 var tmp = x; x++; x = tmp;
  • 现在我有一个很受欢迎的问题,我很遗憾,即使选择了正确的答案,(重复的)答案也不会停止出现。我的“近期活动”屏幕上充满了相同的答案,而且还会有更多...
  • @Rob Vermeulen 您可能想在发表评论之前阅读完整的问题.. ;) 这是我的一个学生编写的代码,我很好奇为什么会出现这种行为。

标签: java loops operators variable-assignment increment


【解决方案1】:

注意:最初我在此答案中发布了 C# 代码以进行说明,因为 C# 允许您通过 ref 关键字的引用传递 int 参数。我决定使用我在 Google 上找到的第一个 MutableInt 类使用实际合法的 Java 代码来更新它,以近似于 ref 在 C# 中所做的事情。我真的不知道这是否有助于或伤害答案。我会说我个人还没有做过那么多的 Java 开发。所以据我所知,可能有更多惯用的方式来说明这一点。


也许如果我们写出一个方法来做类似x++ 所做的事情,它会更清楚。

public MutableInt postIncrement(MutableInt x) {
    int valueBeforeIncrement = x.intValue();
    x.add(1);
    return new MutableInt(valueBeforeIncrement);
}

对吗?将传入的值递增,返回原值:这就是后自增运算符的定义。

现在,让我们看看这种行为在您的示例代码中是如何发挥作用的:

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”这一事实让我感到困惑。分配给y 的不是x;它是以前分配给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 具有值 x0,然后是 x0 + 1,然后又是 x0


更新:顺便说一下,为了避免您怀疑x 是否曾在上面示例中的增量操作和赋值之间分配为 1,我已经整理了一个快速演示来说明这个中间值确实“存在”,尽管它永远不会在执行线程上“看到”。

演示在循环中调用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

【讨论】:

  • 您不需要创建一个类来在 java 中通过引用传递(尽管这肯定会起作用)。您可以使用 Integer 类,它是标准库的一部分,它甚至具有自动装箱到 int几乎 透明的好处。
  • @rmeador Integer 是不可变的,因此您仍然无法更改其值。然而,AtomicInteger 是可变的。
  • @Dan:顺便说一句,你最后一个例子中的x 必须声明为volatile,否则它是一个未定义的行为并且看到1s 是特定于实现的。
  • @burkestar:在这种情况下,我认为链接相当不合适,因为这是一个 Java 问题并且(除非我弄错了)行为实际上是未定义的在 C++ 中。
  • @Tom Brito - 在 C 中它没有定义...++ 可以在分配之前或之后完成。实际上,可能有一个编译器与 Java 做同样的事情,但你不会想赌它。
【解决方案2】:

x = x++ 的工作方式如下:

  • 首先它计算表达式x++。对该表达式的求值会产生一个表达式值(即递增前x 的值)并递增x
  • 稍后它将表达式值分配给x,覆盖增加的值。

因此,事件序列如下所示(这是一个实际的反编译字节码,由javap -c 使用我的 cmets 生成):

 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 // 从栈中弹出值到x

【讨论】:

  • 如果你做一个测试,你可以看到它首先递增,然后是属性。所以它不应该归零。
  • @Tom 这就是重点——因为这都是一个单一的序列,它以不明显(并且可能未定义)的顺序做事。通过尝试对此进行测试,您正在添加一个序列点并获得不同的行为。
  • @Rep 在 C 或 C++ 中可能没有定义,但在 Java 中,已经定义好了。
  • @Jaydee - 几乎......该标准的目的是 符合标准的代码将以相同的方式运行:) 无论如何,有(也许仍然是) 在 C 中的所有可能情况下不指定序列点的优势,但在 Java 中并不是真正的优势。
【解决方案3】:

这是因为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 是基于堆栈的,这意味着对于每个操作,数据将被压入堆栈并从堆栈中弹出数据以执行操作。还有另一种数据结构,通常是存储局部变量的数组。局部变量被赋予 id,它们只是数组的索引。

让我们看看main()方法中的mnemonics

  • iconst_0:常数值0 被压入堆栈。
  • istore_1: 顶部元素 堆栈被弹出并存储在 索引为1的局部变量
    x
  • iload_1 :在 位置1x 的值 0 被压入堆栈。
  • iinc 1, 1 :在 内存位置1 递增1。所以x 现在变成 1
  • istore_1 : 顶部的值 堆栈存储到内存位置1。即分配了0x覆盖它的增量值。

因此x 的值不会改变,导致无限循环。

【讨论】:

  • 其实它是递增的(这就是++的意思),但是变量稍后会被覆盖。
  • int temp = x; x = x + 1; x = temp; 最好不要在您的示例中使用重言式。
【解决方案4】:
  1. 前缀表示法将在计算表达式之前递增变量。
  2. 后缀符号将在表达式评估后递增。

但是,“=”的运算符优先级低于“++”。

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

  1. x 准备分配(评估)
  2. x 递增
  3. x 的先前值分配给 x

【讨论】:

  • 这是最好的答案。一些标记会帮助它更加突出。
  • 这是错误的。这与优先级无关。 ++ 在 C 和 C++ 中的优先级高于 =,但在这些语言中该语句未定义。
  • 原来的问题是关于Java的
【解决方案5】:

没有一个答案很准确,所以这里是:

当您编写int x = x++ 时,您并没有将x 指定为新值,而是将x 指定为x++ 表达式的返回值。这恰好是x 的原始值,正如Colin Cochrane's answer 中所暗示的那样。

为了好玩,测试以下代码:

public class Autoincrement {
        public static void main(String[] args) {
                int x = 0;
                System.out.println(x++);
                System.out.println(x);
        }
}

结果将是

0
1

表达式的返回值为x的初始值,为零。但稍后,当读取x 的值时,我们会收到更新后的值,即1。

【讨论】:

  • 我会尝试理解字节码行,看看我的更新,这样就清楚了.. :)
  • 使用 println() 对我理解这一点很有帮助。
【解决方案6】:

其他人已经解释的很好了。我只包含相关 Java 规范部分的链接。

x = x++ 是一个表达式。 Java 将遵循evaluation order。 它将首先计算表达式 x++,即will increment x and set result value to the previous value of x。 然后它将assign the expression result 传递给变量x。最后,x 又回到了之前的值。

【讨论】:

  • +1。这是迄今为止对实际问题“为什么?”的最佳答案
【解决方案7】:

此声明:

x = x++;

这样评估:

  1. x 推入堆栈;
  2. 递增x;
  3. 从堆栈中弹出x

所以值不变。比较一下:

x = ++x;

计算结果为:

  1. 递增x;
  2. x 推入堆栈;
  3. 从堆栈中弹出x

你想要的是:

while (x < 3) {
  x++;
  System.out.println(x);
}

【讨论】:

  • 绝对是正确的实现,但问题是“为什么?”。
  • 原始代码在 x 上使用后增量,然后将其分配给 x。 x 将在增量之前绑定到 x,因此它永远不会改变值。
  • @cletus 我不是反对者,但您最初的回答没有包含解释。它只是说做'x++`。
  • @cletus:我没有投反对票,但您最初的回答只是x++ 代码 sn-p。
  • 解释也不对。如果代码首先将 x 分配给 x,然后增加 x,它会正常工作。只需将解决方案中的x++; 更改为x=x; x++;,您就可以按照您声称的原始代码正在执行的操作。
【解决方案8】:

答案很简单。它与评估事物的顺序有关。 x++ 返回值 x 然后递增 x

因此,表达式x++ 的值为0。因此,您每次都在循环中分配x=0。当然x++ 会增加这个值,但这发生在赋值之前。

【讨论】:

  • 哇,当答案简短而简单时,这个页面上有这么多细节,即这个。
【解决方案9】:

来自http://download.oracle.com/javase/tutorial/java/nutsandbolts/op1.html

递增/递减运算符可以 在(前缀)之前或之后应用 (后缀)操作数。代码 结果++;和++结果;都会结束 结果加一。 唯一的区别是前缀 版本 (++result) 评估为 增加值,而 后缀版本(结果++)评估 到原来的值。如果你是 只是执行一个简单的 递增/递减,实际上并没有 无论您选择哪个版本。但 如果您在 a 的一部分中使用此运算符 更大的表达,你的那个 选择可能会产生重大影响 区别。

为了说明,请尝试以下操作:

    int x = 0;
    int y = 0;
    y = x++;
    System.out.println(x);
    System.out.println(y);

将打印 1 和 0。

【讨论】:

  • 问题不是评估结果,而是商店的顺序。
  • 我不同意。如果 x = 0,则 x++ 将返回 0。因此 x = x++ 将导致 x = 0。
  • Rup 在这点上是对的。在这种特殊情况下,有争议的是商店的顺序。 y=x++ 与 x=x++ 不同;在后一种情况下,x 在同一个表达式中被分配了 2 个值。左手 x 被赋值为表达式 x++ 的 求值 的结果,即 0。右手边 x 被递增到 1。这 2 个赋值发生的顺序是问题所在.从以前的帖子中可以清楚地看出,它的工作方式是: eval = x++ => eval == 0 : increment right x => x == 1 : left x = eval => x == 0
【解决方案10】:

您并不需要机器代码来了解正在发生的事情。

根据定义:

  1. 赋值运算符计算右侧表达式,并将其存储在临时变量中。

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

    1.2。 x 现在递增。

  2. 临时变量随后被复制到表达式的左侧,即 x 偶然!这就是为什么 x 的旧值再次被复制到自身中的原因。

这很简单。

【讨论】:

    【解决方案11】:

    您实际上得到了以下行为。

    1. 将x的值(即0)作为右边的“结果”
    2. 增加 x 的值(所以 x 现在是 1)
    3. 将右边的结果(原为0)赋值给x(x现在为0)

    这个想法是,后增量运算符 (x++) 在返回其值以用于它所用的方程后,递增该变量。

    编辑:由于评论而添加了一点点。像下面这样考虑它。

    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.
    

    【讨论】:

    • 好的,但是步骤 2 和 3 的顺序是什么?
    • @Rup - 语言定义它。首先计算等式的右侧(在本例中为“x++”),并将结果分配给左侧的变量。这就是语言的工作方式。至于方程的“x++”“返回”x,这就是后缀增量运算符的工作方式(返回 x 的值,然后递增它)。如果它是“--x”,那么它将是(增加 x,然后返回值)。 Return 不是正确的词,但你明白了。
    【解决方案12】:

    这是因为在这种情况下它永远不会增加。 x++ 将首先使用它的值,然后再递增,就像在这种情况下,它会像:

    x = 0;
    

    但如果你这样做++x;,这将增加。

    【讨论】:

    • 如果你做一个测试,你可以看到它首先递增,然后是属性。所以它不应该归零。
    • @Tom:看我的答案——我在测试中显示 x++ 实际上返回 x 的旧值。这就是它崩溃的地方。
    • “如果你做一个测试”——有些人似乎认为用 C 编写的测试告诉我们 Java 会做什么,但它甚至不会告诉我们 C 会做什么。跨度>
    【解决方案13】:

    因为x++ 的值是0,所以值保持在0。在这种情况下x 的值是否增加都没有关系,赋值x=0 被执行。这将覆盖 x 的临时递增值(在“非常短的时间内”为 1)。

    【讨论】:

    • 但是x++是后期操作。所以 x 必须在赋值完成后递增。
    • @Sagar V:仅适用于表达式x++,不适用于整个赋值x=x++;
    • 不,我认为只需要在读取要在赋值中使用的 x 的值后递增。
    【解决方案14】:

    这就像你期望的那样工作。就是前缀和后缀的区别。

    int x = 0; 
    while (x < 3)    x = (++x);
    

    【讨论】:

      【解决方案15】:

      将 x++ 视为一个函数调用,它“返回”X 在增量之前是什么(这就是它被称为后增量的原因)。

      所以操作顺序是:
      1:在递增前缓存x的值
      2:增加 x
      3:返回缓存值(增加前的 x)
      4:返回值赋给x

      【讨论】:

      • 好的,但是第 3 步和第 4 步的顺序是什么?
      • “返回增量之前的 X”是错误的,看我的更新
      • 实际上,第 3 步和第 4 步并不是单独的操作——它不是真正返回值的函数调用,只是这样想会有所帮助。每当你有一个赋值时,右侧被“评估”然后结果被分配给左侧,评估结果可以被认为是一个返回值,因为它可以帮助你理解操作的顺序,但这并不是真的.
      • 哎呀,真的。我的意思是第 2 步和第 4 步 - 为什么返回的值会存储在递增值的顶部?
      • 这是赋值操作定义的一部分,首先完全计算右侧,然后将结果分配给左侧。
      【解决方案16】:

      当 ++ 在 rhs 上时,在数字递增之前返回结果。 换成 ++x 就好了。 Java 会对此进行优化以执行单个操作(将 x 分配给 x)而不是增量。

      【讨论】:

        【解决方案17】:

        据我所见,由于赋值覆盖了增量值,而在增量之前的值,即它撤消了增量,因此发生了错误。

        具体来说,“x++”表达式在递增之前的值为“x”,而“++x”在递增之后的值为“x”。

        如果您有兴趣研究字节码,我们将看看有问题的三行:

         7:   iload_1
         8:   iinc    1, 1
        11:  istore_1
        

        7: iload_1 # 将第二个局部变量的值放入堆栈
        8: iinc 1,1 # 将第二个局部变量增加 1,注意它不会改变堆栈!
        9: istore_1 # 将弹出栈顶并将该元素的值保存到第二个局部变量
        (可以阅读每条JVM指令的效果here

        这就是为什么上面的代码会无限循环,而带有 ++x 的版本不会。 ++x 的字节码应该看起来完全不同,据我记得一年多前编写的 1.3 Java 编译器,字节码应该是这样的:

        iinc 1,1
        iload_1
        istore_1
        

        所以只需交换前两行,就改变了语义,以便在增量之后留在堆栈顶部的值(即表达式的“值”)是增量之后的值。

        【讨论】:

          【解决方案18】:
              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; 相同。

          【讨论】:

            【解决方案19】:
             x = x++; (increment is overriden by = )
            

            因为上面的语句x永远不会达到3;

            【讨论】:

              【解决方案20】:

              我想知道 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
              

              应该是无关紧要的。我认为这两种实现都应该是有效的。

              我会非常谨慎地编写依赖于这种行为的代码。在我看来,它看起来非常依赖于实现,介于规范之间。唯一会产生影响的情况是,如果您做了一些疯狂的事情,例如此处的示例,或者您有两个线程正在运行并且依赖于表达式中的评估顺序。

              【讨论】:

                【解决方案21】:

                我认为因为在 Java ++ 中的优先级高于 =(赋值)...是吗? 看http://www.cs.uwf.edu/~eelsheik/cop2253/resources/op_precedence.html...

                同样的方式,如果你写 x=x+1...+ 的优先级高于 =(赋值)

                【讨论】:

                • 这不是优先级的问题。在 C 和 C++ 中,++ 的优先级也高于 =,但该语句未定义。
                【解决方案22】:

                x++ 表达式的计算结果为 x++ 部分会影响 evaluation 之后的值,而不是 statement 之后的值。所以x = x++ 被有效地翻译成

                int y = x; // evaluation
                x = x + 1; // increment part
                x = y; // assignment
                

                【讨论】:

                  【解决方案23】:

                  在值加一之前,将值赋给变量。

                  【讨论】:

                    【解决方案24】:

                    它正在发生,因为它的帖子增加了。这意味着变量在表达式被计算后递增。

                    int x = 9;
                    int y = x++;
                    

                    x 现在是 10,但 y 是 9,x 增加之前的值。

                    Definition of Post Increment 中查看更多信息。

                    【讨论】:

                    • 您的x/y 示例与真实代码不同,差异相关。您的链接甚至没有提到 Java。对于它确实提到的两种语言,问题中的陈述是未定义的。
                    【解决方案25】:

                    检查下面的代码,

                        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 并且这是在一个循环中(第一行代码除外)怎么办。就像问题一样!!!!

                    【讨论】:

                      【解决方案26】:

                      增量运算符应用于与您分配相同的变量。那是自找麻烦。我相信你可以在运行这个程序时看到你的 x 变量的值......这应该清楚为什么循环永远不会结束。

                      【讨论】:

                        猜你喜欢
                        • 2011-11-21
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 2020-04-28
                        • 1970-01-01
                        • 1970-01-01
                        • 2010-11-01
                        相关资源
                        最近更新 更多