为什么这个程序会进入无限循环?

509

我有以下的代码:

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

}

我会阅读有关指令的内容,以尝试理解...


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++ 编译器。 - grieve
14
@saj,x++是后自增;x=结果的赋值;x++的结果是原始的x(虽然有一个增量的副作用,但这不会改变结果),因此可以解释为var tmp = x; x++; x = tmp; - Marc Gravell
5
既然我有一个受欢迎的问题,我感到后悔了。即使选择了正确答案,重复的回答仍不停地涌现。我的“最近活动”页面满是相同的答案,而且还在继续增加... - The Student
3
@Rob Vermeulen,在发表评论之前,您可能需要阅读完整的问题.. ;) 这是我的一个学生编写的代码,我想知道这种行为的原因。 - The Student
显示剩余18条评论
26个回答

361

注意: 最初我在这个答案中发布了C#代码以说明目的,因为C#允许您使用ref关键字通过引用传递int参数。我决定更新为实际合法的Java代码,使用我在Google上找到的第一个MutableInt类来近似表示C#中ref的功能。我无法确定这是否有助于或损害了答案。我个人认为我没有做过太多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

3
整数是不可变的,因此您仍然无法更改其值。然而,AtomicInteger是可变的。 - ILMTitan
5
顺便说一下,你最后一个例子中的 x 必须声明为 volatile,否则它将是未定义行为,看到 1 可能是实现特定的。 - axtavt
4
@burkestar: 我认为在这种情况下那个链接不太合适,因为这是一个关于Java的问题,而且(除非我搞错了)这种行为在C++中实际上是未定义的。 - Dan Tao
5
@Tom Brito - 在C语言中,这并没有被定义...... ++ 可以在赋值之前或之后完成。实际上,可能有编译器与Java做相同的事情,但你不想赌它会发生什么。 - detly
1
这是一种非常冗长的说法,即赋值表达式的右侧先进行求值。 - Jason C
显示剩余7条评论

175

x = x++ 操作的过程如下:

  • 首先,会评估表达式 x++ 的值。这个表达式的值为表达式被增加前的x的值,同时也会将x的值增加。
  • 然后,它将表达式的值赋给x,覆盖掉增加后的值。

因此,事件序列看起来像下面这样(这是由javap -c生成的实际反编译字节码,带有我的注释):

   8:   iload_1         // 将x当时的值压入栈中
   9:   iinc    1, 1    // 增加x(不改变栈)
   12:  istore_1        // 将栈中存储的x的原值写回x

相比之下,x = ++x 的操作过程如下:

   8:   iinc    1, 1    // 增加x
   11:  iload_1         // 将x的值压入栈中
   12:  istore_1        // 将栈中的值弹出并写回x

2
@Tom,这就是重点——因为这是一个单一的序列,它以一种非明显(可能是未定义的)的顺序执行操作。通过尝试测试它,您正在添加一个序列点并获得不同的行为。 - Rup
4
在 C 或 C++ 中可能没有定义,但在 Java 中,这一点被定义得很好。 - ILMTitan
@Detly 你说得对,我已经有机会查看了语言规范。不过我觉得它很奇怪。 - Jaydee
1
@Jaydee - 差不多了...标准的目的是符合标准的代码将以相同的方式运行 :) 无论如何,在C语言中不在每种可能情况下指定序列点有优势(也许现在仍然存在),但在Java中并非如此。 - detly
1
有趣的文章 http://www.angelikalanger.com/Articles/VSJ/SequencePoints/SequencePoints.html - Jaydee
显示剩余9条评论

106

这是因为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虚拟机(Java VM)是基于栈的,这意味着对于每个操作,数据将被推送到堆栈上,然后从堆栈中弹出数据执行操作。还有另一个数据结构,通常是用于存储本地变量的数组。本地变量被赋予ID,这些ID只是数组索引。
让我们看一下main()方法中的助记符
  • iconst_0:将常量值 0 推送到堆栈上。
  • istore_1:将堆栈的顶部元素弹出并存储在具有索引 1 的本地变量中,即x
  • iload_1:将位置 1 处的值(即x的值0)推送到堆栈上。
  • iinc 1,1:将内存位置 1 处的值增加 1 。因此,x现在变为 1
  • istore_1:将堆栈顶部的值存储到内存位置 1 中。也就是,0被分配给x,从而覆盖了其增加后的值。
因此,x的值不会改变,导致产生无限循环。

6
实际上它被递增了(这就是 ++ 的含义),但变量后来被覆盖了。 - Progman
10
int temp = x; x = x + 1; x = temp; - Scott Chamberlain

54
  1. 前缀表示法将在计算表达式之前递增变量。
  2. 后缀表示法将在表达式计算完成之后递增。

然而,"="的运算符优先级低于"++"。

因此,x=x++;应该按照以下方式进行计算:

  1. x准备分配(已计算)
  2. x递增
  3. x的先前值分配给x

这是最好的答案。一些标记会帮助它更加突出。 - Justin Force
1
иҝҷжҳҜй”ҷиҜҜзҡ„гҖӮиҝҷдёҺдјҳе…Ҳзә§ж— е…ігҖӮеңЁCе’ҢC++дёӯпјҢ++зҡ„дјҳе…Ҳзә§й«ҳдәҺ=пјҢдҪҶиҜҘиҜӯеҸҘеңЁиҝҷдәӣиҜӯиЁҖдёӯжҳҜжңӘе®ҡд№үзҡ„гҖӮ - Matthew Flaschen
3
原始问题是关于Java的。 - Jaydee

34

没有一个答案完全正确,所以接下来就是我的解释:

当你写 int x = x++ 时,你并不是将x赋值为新值的自身,而是将x赋值为x++表达式的返回值。正如Colin Cochrane's answer中所提示的那样,这个返回值恰好是x的原始值。

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

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
我会尝试理解字节码行,看看我的更新,这样就清楚了.. :) - The Student
1
使用println()对我理解这个非常有帮助。 - ErikE

29

2
+1. 这绝对是对于实际问题“为什么?”最好的回答。 - Matthew Flaschen

18
这个声明:
x = x++;

执行过程如下:

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

因此,值没有改变。与此相比:

x = ++x;

这将被计算为:

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

你想要的是:

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

14
正确的实现,但问题是“为什么?”。 - p.campbell
2
原始代码在 x 上使用后增操作,然后将其分配给 x。x 将绑定到增量之前的 x,因此它永远不会更改值。 - wkl
6
@cletus 我不是那个给你点踩的人,但是你最初的回答没有解释。它只是说要做'x++'。 - Petar Minchev
5
@cletus说:“我没有投反对票,但你的答案最初只是 x++ 这段代码片段。” - p.campbell
11
解释也是不正确的。如果代码首先将 x 分配给自身,然后再递增 x,它就可以正常工作。只需将您的解决方案中的 x++; 更改为 x=x; x++;,那么您就正在执行原始代码所要求的操作。 - Wooble
显示剩余4条评论

10
答案非常简单。它与评估顺序有关。x++首先返回值x,然后增加x
因此,表达式x++的值为0。因此,每次循环中都会将x=0赋值。当然,x++会增加这个值,但是这发生在赋值之前。

2
哇,这个页面上有很多细节,但答案却很简单,就是这个。 - Charles Goodwin

8

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

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

为了说明,请尝试以下内容:

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

这将打印出1和0。


2
问题不在于评估结果,而在于商店的顺序。 - Rup
3
我不同意。如果x = 0,那么x++将返回0。因此x = x++会导致x = 0。 - Colin Cochrane
1
Rup在这方面是正确的。在这个特定的情况下,问题在于商店的顺序。y=x++并不等同于x=x++; 在后者中,x在同一表达式中被分配了2个值。左手边的x被分配了表达式x++的评估结果,即0。右手边的x被增加到1。这两个赋值发生的顺序是问题所在。从之前的帖子中可以清楚地看出,它的工作方式是:eval = x++ => eval == 0: 增加右侧x => x == 1: 左侧x = eval => x == 0 - Michael Ekoka

8
你不需要机器码就能理解发生了什么。
根据定义:
1. 赋值运算符评估右侧表达式,并将其存储在临时变量中。 1.1. x的当前值被复制到此临时变量中。 1.2. x现在被增加。 2. 然后将临时变量复制到左侧的表达式中,这个表达式恰好是x!所以旧值x再次被复制到自身。
这很简单。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接