为什么会变成一个无限循环呢?

我有以下代码:

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属性赋给自己,然后再对其加1。为什么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


}

我将阅读指令来尝试理解…

24783 次浏览

这句话:

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 = 0;

但是如果你做++x;,这个会增加。

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

  • 首先,它计算表达式x++。这个表达式的求值产生一个表达式值(它是x在increment之前的值),并使x增加。
  • 之后,它将表达式值赋给x,覆盖增量值。

因此,事件序列如下所示(它是一个实际的反编译字节码,由javap -c生成,带有我的注释):

   8:   iload_1         // Remember current value of x in the stack
9:   iinc    1, 1    // Increment x (doesn't change the stack)
12:  istore_1        // Write remebered value from the stack to x

作为比较,x = ++x:

   8:   iinc    1, 1    // Increment x
11:  iload_1         // Push value of x onto stack
12:  istore_1        // Pop value from the stack to 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是基于堆栈的,这意味着对于每个操作,数据将被推入堆栈,并且从堆栈中,数据将弹出来执行操作。还有另一种数据结构,通常是存储局部变量的数组。局部变量的id是数组的索引。

让我们看看main()方法中的助记符:

  • iconst_0:常量值0
  • 类的顶部元素 堆栈弹出并存储在 索引1
    的局部变量 李是x。< / >
  • iload_1:在 location 1,即x的值 0被压入堆栈。
  • iinc 1, 1:在 内存位置1增加1。所以x现在变成 李1。< / > istore_1:顶部的值 堆栈被存储到内存位置__abc0。即赋值0 x < em >覆盖< / em >它的增量值

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

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

把x++看作是一个函数调用,它“返回”x的之前增量(这就是为什么它被称为后增量)。

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

你有效地得到了以下行为。

  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.

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

递增/递减操作符可以 应用于(前缀)之前或之后 (后缀)操作数。的代码 结果+ +;+ +的结果;都会结束 结果是加1。 唯一的区别是前缀 版本(++结果)计算为 增量值,,而 后缀版本(result++)计算 到原值。如果你是 只是执行一个简单的 递增/递减,不是这样的 不管你选择哪个版本。但 如果你在a的部分使用这个运算符 大一点的表情,就是你 选择可能会做出重大的决定 区别。< / p >

为了说明这一点,请尝试以下方法:

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

它会输出1和0。

  1. 前缀表示法将在表达式求值之前增加变量。
  2. 后缀表示法将在表达式求值后增加。

但是"="的运算符优先级比"++"低。

因此x=x++;的值应该如下所示

  1. x准备赋值(已求值)
  2. x递增
  3. x之前的值分配给x

没有一个答案是完全正确的,所以是这样的:

当你在编写int x = x++时,你并没有将x赋值为新值本身,而是将x赋值为x++表达式的返回值。恰好是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

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

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

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

请注意:最初我在这个答案中发布了c#代码,用于演示,因为c#允许你通过引用ref关键字来传递int参数。我决定用实际合法的Java代码更新它,使用我在谷歌上找到的第一个MutableInt类,以近似于ref在c#中的作用。我真的不知道这对答案是有帮助还是有害。我得说,我个人没有做过那么多Java开发;所以据我所知,可以有更多的习语来说明这一点。


也许如果我们写一个方法来做与x++所做的等价的事情,就会更清楚。

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

对吧?对传递的值加1并返回原始值:这就是后加1操作符的定义。

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

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.

当你用y替换上面赋值的左边的x时,“你可以看到它首先增加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的不规则出现。

Starting background thread...
0
0
1
1
0
0
0
0
0
0
0
0
0
0
1
0
1

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

x = x++是一个表达式。Java将跟随评估顺序。 它将首先计算表达式x++, 是否增加x并将结果值设置为x的前一个值.; 然后它将分配表达式结果转换为变量x。最后,x返回到它之前的值

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

因为上面的表述x永远不会达到3;

我想知道Java规范中是否有精确定义此行为的内容。(这句话的明显意思是我懒得去检查。)

注意Tom的字节码,关键行是7、8和11。第7行将x加载到计算堆栈中。第8行增加x。第11行将堆栈的值存储回x。在正常情况下,如果你不将值赋值回自身,我不认为有任何理由不能加载,存储,然后增加。你会得到同样的结果。

比如,假设你有一个更正常的情况,你写了这样的东西: z = (x + +) + (y + +); < / p >

它是否说(跳过技术细节的伪代码)

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

应该无关紧要。我认为,任何一种实现都是有效的。

我会非常谨慎地编写依赖于这种行为的代码。对我来说,它看起来非常依赖于实现。只有当您做了一些疯狂的事情(比如这里的例子),或者您有两个线程正在运行,并且依赖于表达式中的求值顺序时,才会产生影响。

当++在rhs上时,在数字增加之前返回结果。 更改为++x就可以了。 Java本可以将其优化为执行单个操作(将x赋值给x),而不是增量操作

你不需要机器代码来理解发生了什么。

根据定义:

  1. 赋值操作符计算右边的表达式,并将其存储在一个临时变量中。

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

    1.2.

  2. 然后将临时变量复制到表达式的左侧,也就是x !这就是为什么原来的x值又被复制到自身。

这很简单。

我想是因为在java++中有比=(赋值)更高的优先级…不是吗? 查看http://www.cs.uwf.edu/~eelsheik/cop2253/resources/op_precedence.html

同理,如果x=x+1。+的优先级高于=(赋值)

在值加1之前,值被赋值给变量。

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

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

这是因为它是后增量的。它意味着变量在表达式求值之后递增。

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

X现在是10,但y是9,这是X加之前的值。

详见后增量的定义

答案很简单。它和求值的顺序有关。x++返回值x,然后增加x

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

据我所知,错误发生了,由于赋值覆盖了增量值,在增量之前的值,即它取消了增量。

具体来说,"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

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

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

    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;是相同的。

检查下面的代码,

    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是在一个循环中(除了第一行代码)。就像问题!!!!一样