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


493

我有以下代码:

public class Tests {
    public static void main(String[] args) throws Exception {
        int x = 0;
        while(x<3) {
            x = x++;
            System.out.println(x);
        }
    }
}

我们知道他应该已经写了just 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

}

我将阅读说明以尝试理解...


8
我怀疑正在发生什么:1.将x加载到寄存器(= 0);2.增量x(x = 1); 3.将寄存器值保存到x(x = 0)中。在C / C ++中,这是未定义的行为,因为没有正式的顺序点来定义2和3的顺序。希望有人可以引用Java规范中的等效内容。
Rup

19
我们在C ++中尝试了此操作,以查看会发生什么,并输出1,2,3并退出。我原本没想到。我认为它是编译器相关的,因为它是未定义的行为。我们使用了gnu g ++。
悲伤

13
@saj x++是后递增的;x=结果的分配; 该结果x++是原来的x(且有增量的副作用,但是,这并不改变结果),所以这可以被解释为var tmp = x; x++; x = tmp;
马克·Gravell

5
既然我有一个很普遍的问题,我感到很遗憾,即使选择了正确的答案,(重复的)答案也不会停止。我的“最近活动”屏幕上充满了相同的答案,并且还会有更多...
汤姆·布里托

3
@Rob Vermeulen,您可能要在评论前先阅读完整的问题。;;)这是我的一个学生编写的代码,我很好奇为什么这样做。
汤姆·布里托

Answers:


354

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


也许如果我们写出一种方法来做与之等效的事情x++,这将使事情变得更清楚。

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

对?递增传递的值并返回原始值:这是postincrement运算符的定义。

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

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,我整理了一个快速演示以说明该中间值确实“存在”,尽管它将永远不会在执行线程上被“看到”。

该演示程序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个

1
您不需要创建一个类来通过java中的引用进行传递(尽管那肯定可以工作)。您可以使用Integer类,它是标准库的一部分,它甚至具有int 几乎透明地自动装箱和装箱的优点。
rmeador

3
@rmeador整数是不可变的,因此您仍然无法更改其值。但是,AtomicInteger是可变的。
ILMTitan

5
@Dan:顺便说一句,x在您的最后一个示例中必须声明volatile,否则它是未定义的行为,并且看到1s是实现特定的。
axtavt 2010年

4
@burkestar:在这种情况下,我认为该链接不太合适,因为这是一个Java问题,并且(除非我没有记错)该行为实际上在C ++中未定义。
丹涛

5
@Tom Brito-在C语言中未定义... ++ 可以在分配之前或之后完成。实际上,可能会有一个编译器执行与Java相同的功能,但是您不希望对其打赌。
不错,2010年

170

x = x++ 以下列方式工作:

  • 首先,它评估表达式x++。对该表达式求值会生成一个表达式值(即x递增之前的值)和增量x
  • 之后,它将表达式值分配给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 //将值从堆栈弹出到x

如果进行测试,则可以看到它先增加,然后再增加属性。因此,它不应归零。
汤姆·布里托

2
@Tom就是重点,因为这都是单个序列,因此它以非显而易见(可能未定义)的顺序工作。通过尝试对此进行测试,您将添加一个序列点并获得不同的行为。
Rup

关于字节码输出:请注意,iinc增加变量,不会增加堆栈值,也不会在堆栈上留下值(与几乎所有其他算术运算符不同)。您可能需要添加生成的代码以++x进行比较。
Anon 2010年

3
@Rep可能没有用C或C ++定义,但是在Java中,它定义良好。
ILMTitan 2010年


104

发生这种情况是因为的值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()方法:

  • iconst_0:将常量值0 压入堆栈。
  • istore_1:弹出堆栈的顶部元素,并将其存储在索引1
    为的局部变量 中x
  • iload_1:在该位置处的值1是值的x0,被压入堆栈。
  • iinc 1, 1:存储位置的1值增加1。所以x现在变成了 1
  • istore_1:堆栈顶部的值存储到存储位置1。这是0被分配到x 覆盖其增加后的值。

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


5
实际上,它会增加(即的含义++),但稍后会覆盖该变量。
Progman 2010年

10
int temp = x; x = x + 1; x = temp;最好不要在您的示例中使用重言式。
Scott Chamberlain 2010年

52
  1. 在计算表达式之前,前缀符号将使变量递增。
  2. 后缀表示法将在表达式求值后增加。

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

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

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

这是最好的答案。一些标记会帮助它脱颖而出。
贾斯汀力

1
错了 这与优先权无关。 ++具有比=C和C ++ 更高的优先级,但是在这些语言中该语句未定义。
马修·弗拉申

2
原来的问题是关于Java
杰德

34

没有一个答案很明显,所以去了:

在编写时int x = x++,您没有分配x给自己为新值,而是分配xx++表达式的返回值。x正如科林·科克伦(Colin Cochrane)的回答所暗示的,这恰好是的原始值。

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

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

结果将是

0
1

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


我将尝试了解字节码行,请参阅我的更新,这样会很清楚.. :)
Tom Brito 2010年

使用println()对我的理解非常有帮助。
ErikE 2010年


18

这个说法:

x = x++;

评估如下:

  1. x入堆栈;
  2. 增量x;
  3. x从堆栈中弹出。

因此该值不变。比较一下:

x = ++x;

其评估为:

  1. 增量x;
  2. x入堆栈;
  3. x从堆栈中弹出。

您想要的是:

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

13
绝对正确的实现,但是问题是“为什么?”。
p.campbell 2010年

1
原始代码在x上使用后递增,然后将其分配给x。x将在递增之前绑定到x,因此它将永远不会更改值。
wkl 2010年

5
@cletus我不是拒绝投票的人,但您的初始答案没有解释。它只是说“ x ++”。
Petar Minchev 2010年

4
@cletus:我没有拒绝投票,但您的答案本来只是x++代码片段。
p.campbell 2010年

10
解释也不正确。如果代码首先将x分配给x,然后递增x,则可以正常工作。只需更改x++;您的解决方案x=x; x++;,您就可以按照原始代码的要求进行操作。
Wooble 2010年

10

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

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


1
哇,答案很简单也很简单,即此页面上有太多详细信息。
Charles Goodwin 2014年

8

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

可以在操作数(前缀)之前或之后(后缀)应用增量/减量运算符。代码结果++; 和++结果;都将导致结果加一。唯一的区别是前缀版本(++ result)的值等于增量值,而后缀版本(result ++)的值等于原始值。如果您只是执行简单的增量/减量,则选择哪个版本都没有关系。但是,如果在较大表达式的一部分中使用此运算符,则选择的运算符可能会产生很大的不同。

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

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

它将打印1和0。


1
问题不是评估结果,而是商店的顺序。
Rup

2
我不同意。如果x = 0,则x ++将返回0。因此X = X ++将导致x = 0的
科林循证医学

鲁普对此是正确的。在这种特殊情况下,商店的顺序是有争议的。y = x ++与x = x ++不同;在后一种情况下,x在同一表达式中被分配了2个值。左手x被赋值为表达式x ++ 的求值结果,该表达式为0。右手x被递增为1。这2个赋值发生的顺序是问题所在。从以前的帖子中可以清楚地看出,它的工作方式是:eval = x ++ => eval == 0:向右递增x => x == 1:向左x = eval => x == 0
Michael Ekoka 2010年

7

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

  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

@Rup-语言定义它。首先评估方程式的右侧(在本例中为“ x ++”),然后将结果分配给左侧的变量。这就是语言的工作方式。就等式的“ x ++”“返回” x而言,这就是后缀增量运算符的工作方式(返回x的值,然后对其进行递增)。如果它是“ --x”,则它应该是(递增x,然后返回值)。返回不是正确的词,但是您知道了。
RHSeeger 2010年

7

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

根据定义:

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

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

    1.2。x现在增加。

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

这很简单。


5

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

x = 0;

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


如果进行测试,则可以看到它先增加,然后再增加属性。因此,它不应归零。
汤姆·布里托

1
@Tom:看看我的答案-我在测试中证明x ++实际上返回了x的旧值。那就是它的坏处。
罗伯特·蒙蒂亚努

“如果您进行测试” –某些人似乎认为用C编写的测试可以告诉我们Java会做什么,甚至不告诉我们C会做什么。
Jim Balter

3

该值保持为0,因为的值为x++0。在这种情况下,x是否增加值并不重要,将x=0执行分配。这将覆盖临时增加的值x(“非常短的时间”为1)。


但是x ++是后期操作。因此,x必须在分配完成后增加。
Sagar V

1
@Sagar V:仅用于表达x++,不适用于整个作业x=x++;
Progman 2010年

不,我认为仅在读取要在分配中使用的x的值后才需要增加它。
Rup

1

这可以像您期望的那样工作。这是前缀和后缀之间的区别。

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

1

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

因此操作顺序为:
1:在递增之前缓存x的值
2:递增x
3:返回缓存的值(递增之前的x)
4:返回值分配给x


好的,但是什么指定了步骤3和4的顺序?
Rup

“返回增量之前的X”是错误的,请参见我的更新
Tom Brito 2010年

在现实步骤3和4不是单独的操作-它不是真正的函数调用返回的值,它只是帮助这样想起来了。每当您进行赋值时,都会对右侧进行“求值”,然后将结果赋给左侧,可以将求值结果视为返回值,因为它可以帮助您理解操作顺序,但实际上并非如此。
jhabbott

糟糕,是的。我的意思是第2步和第4步-为什么返回的值存储在递增值的顶部?
Rup

1
这是分配操作定义的一部分,首先完全评估右侧,然后将结果分配给左侧。
jhabbott 2010年

1

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


1

据我所知,由于赋值将增量值替换为增量之前的值,导致发生错误,即取消增量。

具体来说,“ 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指令的效果)

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

iinc 1,1
iload_1
istore_1

因此,仅交换前两行,即可更改语义,以使增量后的值(即表达式的“值”)保留在堆栈顶部,即增量后的值。


1
    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;在线本身相同。


0
 x = x++; (increment is overriden by = )

由于上述陈述,x永远不会达到3;


0

我想知道Java规范中是否有任何东西可以精确定义其行为。(该声明的明显含义是我懒得检查。)

从汤姆的字节码中注意到,关键行是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

应该无关紧要。我认为,任何一种实施方式都应该有效。

对于编写依赖于此行为的代码,我会非常谨慎。在我看来,它看起来非常依赖于实现,在规范之间。唯一会有所不同的是,您是否做了疯狂的事情(例如此处的示例),或者是否有两个线程正在运行并且取决于表达式中的求值顺序。



0

x++表达式的计算结果为x。该++部分会影响评估后的,而不是声明后的值。所以x = x++有效地翻译成

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


0

这是因为帖子增加了。这意味着在对表达式求值后,变量将递增。

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

x现在为10,但y为9,即x递增之前的值。

请参阅“职位增量定义”中的更多内容。


1
您的x/ y示例与实际代码不同,并且不同是相关的。您的链接甚至没有提到Java。二是语言的提,在这一问题的声明是不明确的。
Matthew Flaschen

0

检查以下代码,

    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和这在循环中(除了第一行代码),该怎么办?就像问题中一样!!!!


-1

增量运算符将应用于您分配给的相同变量。那是麻烦。我确信您可以在运行该程序时看到x变量的值。这应该清楚说明循环永远不会结束的原因。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.