我有以下代码:
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 以尝试理解...
x++
是后增量; x=
是 result 的赋值; x++
的 result 是原始的 x
(并且存在增量的副作用,但不会改变结果),因此可以将其解释为 var tmp = x; x++; x = tmp;
注意:最初我在此答案中发布 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
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
发生这种情况是因为 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;
最好不要在您的示例中使用重言式。
前缀表示法将在计算表达式之前递增变量。后缀符号将在表达式评估之后递增。
但是,“=
”的运算符优先级低于“++
”。
所以 x=x++;
应该评估如下
准备分配(评估) x 递增 x 的先前值分配给 x。
++
在 C 和 C++ 中的优先级高于 =
,但在这些语言中该语句未定义。
没有一个答案很明显,所以这里是:
当您编写 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。
其他人已经解释的很好了。我只包括相关 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 又回到了之前的值。
这个说法:
x = x++;
评估如下:
将 x 压入堆栈;增加 x;从堆栈中弹出 x。
所以值不变。将其与以下内容进行比较:
x = ++x;
评估为:
增加 x;将 x 压入堆栈;从堆栈中弹出 x。
你想要的是:
while (x < 3) {
x++;
System.out.println(x);
}
x++
代码片段。
x++;
更改为 x=x; x++;
,您就可以按照您声称的原始代码正在执行的操作进行操作。
答案很简单。它与评估事物的顺序有关。 x++
返回值 x
,然后递增 x
。
因此,表达式 x++
的值为 0
。因此,您每次都在循环中分配 x=0
。当然 x++
会增加这个值,但这发生在赋值之前。
来自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.1。 x 的当前值被复制到这个临时变量 1.2 中。 x 现在递增。然后临时变量被复制到表达式的左侧,即 x 偶然!这就是为什么 x 的旧值再次被复制到自身中的原因。
这很简单。
您有效地获得了以下行为。
获取 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.
这是因为在这种情况下它永远不会增加。 x++
将首先使用它的值,然后再递增,就像在这种情况下,它会像:
x = 0;
但是如果你这样做++x;
,这会增加。
由于x++
的值为0,因此该值保持为0。在这种情况下,无论x
的值是否增加,都将执行赋值x=0
。这将覆盖 x
的临时递增值(在“非常短的时间内”为 1)。
x++
,不适用于整个赋值 x=x++;
这可以按照您对另一个人的期望进行。这是前缀和后缀之间的区别。
int x = 0;
while (x < 3) x = (++x);
将 x++ 视为一个函数调用,它“返回”X 在增量之前是什么(这就是它被称为后增量的原因)。
所以操作顺序是: 1:在递增前缓存x的值 2:递增x 3:返回缓存的值(递增前的x) 4:将返回值赋给x
当 ++ 在 rhs 上时,在数字递增之前返回结果。换成 ++x 就好了。 Java 会对此进行优化以执行单个操作(将 x 分配给 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指令的效果here)
这就是为什么上面的代码会无限循环,而带有 ++x 的版本不会。 ++x 的字节码看起来应该完全不同,据我记得一年多前编写的 1.3 Java 编译器,字节码应该是这样的:
iinc 1,1
iload_1
istore_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;
相同。
x = x++; (increment is overriden by = )
由于上述语句 x 永远不会达到 3;
我想知道 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
应该无关紧要。我认为,任何一种实现都应该是有效的。
对于编写依赖于这种行为的代码,我会非常谨慎。在我看来,它看起来非常依赖于实现,介于规范之间。唯一会产生影响的情况是您是否做了一些疯狂的事情,例如此处的示例,或者您有两个线程正在运行并且依赖于表达式中的评估顺序。
我认为因为在 Java ++ 中的优先级高于 =(赋值)...是吗?看http://www.cs.uwf.edu/~eelsheik/cop2253/resources/op_precedence.html...
同样,如果你写 x=x+1...+ 的优先级高于 = (assignment)
++
在 C 和 C++ 中的优先级也高于 =
,但该语句未定义。
x++
表达式的计算结果为 x
。 ++
部分影响 evaluation 之后的值,而不是 statement 之后的值。所以 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 增加之前的值。
在 Definition of Post Increment 中查看更多信息。
x
/y
示例与实际代码不同,并且差异是相关的。您的链接甚至没有提到 Java。对于它确实提到的两种语言,问题中的陈述是未定义的。
检查下面的代码,
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
并且这是在一个循环中(第一行代码除外)怎么办。就像问题一样!!!!
增量运算符应用于您分配的同一变量。那是自找麻烦。我相信你可以在运行这个程序时看到你的 x 变量的值......这应该清楚为什么循环永远不会结束。
Integer
类,它是标准库的一部分,它甚至具有自动装箱到int
几乎透明的好处。x
必须声明为volatile
,否则它是未定义的行为,并且看到1
是特定于实现的。++
可以 在分配之前或之后完成。实际上,可能有一个编译器与 Java 做同样的事情,但你不会想赌它。