ChatGPT解决这个技术问题 Extra ChatGPT

为什么会进入无限循环?

我有以下代码:

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 您可能想在发表评论之前阅读完整的问题.. ;) 这是我的一个学生编写的代码,我很好奇为什么会出现这种行为。

D
Dan Tao

注意:最初我在此答案中发布 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,我整理了一个快速演示来说明这个中间value 确实“存在”,尽管它永远不会在执行线程上“看到”。

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

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

首先它计算表达式 x++。对该表达式的求值会产生一个表达式值(即递增前 x 的值)并递增 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

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

发生这种情况是因为 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 :位置 1 的值,即 x 的值为 0,被压入堆栈。

iinc 1, 1 :内存位置 1 的值增加 1。所以 x 现在变为 1。

istore_1 :堆栈顶部的值存储到内存位置1。也就是说,将 0 分配给 x 以覆盖其增加的值。

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


实际上它是递增的(这就是 ++ 的含义),但变量稍后会被覆盖。
int temp = x; x = x + 1; x = temp; 最好不要在您的示例中使用重言式。
h
heavyd

前缀表示法将在计算表达式之前递增变量。后缀符号将在表达式评估之后递增。

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

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

准备分配(评估) x 递增 x 的先前值分配给 x。


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

没有一个答案很明显,所以这里是:

当您编写 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。


我会尝试理解字节码行,看看我的更新,所以它会很清楚.. :)
使用 println() 对我理解这一点非常有帮助。
p
plodoc

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

= 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。这是迄今为止对实际问题“为什么?”的最佳答案。
c
cletus

这个说法:

x = x++;

评估如下:

将 x 压入堆栈;增加 x;从堆栈中弹出 x。

所以值不变。将其与以下内容进行比较:

x = ++x;

评估为:

增加 x;将 x 压入堆栈;从堆栈中弹出 x。

你想要的是:

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

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

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

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


哇,当答案简短而简单时,此页面上有很多细节,即这个。
C
Colin Cochrane

来自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。


问题不是评估结果,而是商店的顺序。
我不同意。如果 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
P
Peter Mortensen

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

根据定义:

赋值运算符计算右侧表达式,并将其存储在临时变量中。 1.1。 x 的当前值被复制到这个临时变量 1.2 中。 x 现在递增。然后临时变量被复制到表达式的左侧,即 x 偶然!这就是为什么 x 的旧值再次被复制到自身中的原因。

这很简单。


P
Peter Mortensen

您有效地获得了以下行为。

获取 x 的值(即 0)作为右侧的“结果”递增 x 的值(因此 x 现在为 1)将右侧的结果(保存为 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,然后返回值)。那里的回报不是正确的词,但你明白了。
P
Peter Mortensen

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

x = 0;

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


如果您进行测试,您可以看到它首先递增,然后是属性。所以它不应该归零。
@Tom:看我的答案 - 我在测试中显示 x++ 实际上返回 x 的旧值。那就是它破裂的地方。
“如果你做一个测试”——有些人似乎认为用 C 编写的测试告诉我们 Java 会做什么,而它甚至不会告诉我们 C 会做什么。
P
Progman

由于x++ 的值为0,因此该值保持为0。在这种情况下,无论x 的值是否增加,都将执行赋值x=0。这将覆盖 x 的临时递增值(在“非常短的时间内”为 1)。


但是 x++ 是一个后期操作。所以 x 必须在分配完成后递增。
@Sagar V:仅适用于表达式 x++,不适用于整个赋值 x=x++;
不,我认为它只需要在读取要在分配中使用的 x 的值后递增。
p
patrick

这可以按照您对另一个人的期望进行。这是前缀和后缀之间的区别。

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

j
jhabbott

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

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


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

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


m
micdah

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

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

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


P
Paul
    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; 相同。


P
Praveen Prasad
 x = x++; (increment is overriden by = )

由于上述语句 x 永远不会达到 3;


J
Jay

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

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

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


c
cubob

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

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


这不是优先级的问题。 ++ 在 C 和 C++ 中的优先级也高于 =,但该语句未定义。
t
tia

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

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

P
Peter Mortensen

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


P
Peter Mortensen

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

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

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

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


您的 x/y 示例与实际代码不同,并且差异是相关的。您的链接甚至没有提到 Java。对于它确实提到的两种语言,问题中的陈述是未定义的。
p
prime

检查下面的代码,

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


S
SIVAKUMAR.J

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