在Java中,a = (a++) * (a++)会产生奇怪的结果。

45

我正在准备 OCPJP 考试,因此必须了解 Java 的每一个细节。这包括前缀和后缀增量运算符应用于变量的顺序。以下代码给出了奇怪的结果:

int a = 3;

a = (a++) * (a++);

System.out.println(a); // 12

答案难道不应该是11吗?或者可能是13?但绝不是12!

接下来的问题:

以下代码的结果是什么?

int a = 3;

a += (a++) * (a++);

System.out.println(a);

58
没有人应该编写这样的代码。相信编译器——它比你更了解Java。我认为这是正确的:第一次计算返回3,第二次返回4。最终a的值为5。 - duffymo
9
@duffymo: 这就是我对OCJP考试中的每个问题的看法。 - Pops
57
如果你说希望11或13成为结果,我不明白为什么会这样想,因为11和13都是质数。如果你说"9",我可以理解你的困惑,有点道理。 - Brian Roach
14
需要你知道这种无聊的东西才能通过考试? - Oliver Charlesworth
22
这是后置自增操作,因此它的含义是 a = 3 (->4) * 4 (->5) // = 3*4 = 12 - Smamatti
显示剩余24条评论
15个回答

109

第一次执行a++后,a的值变为4。所以你有3 * 4 = 12

(第二次执行a++后,a变成5,但它被覆盖掉了,因为赋值操作a=会覆盖它)


2
我非常确定在这种情况下是从左到右的。您可以通过将第一个a++替换为(one() + a++)并将第二个替换为(two() + a++)来证明它。使one()和two()都返回0,并在这些方法中放置println。在运行时,您将看到先调用one()再调用two()。 - Mike
1
很酷,我从没想到结果可以被丢弃。我的想法是最后的++会应用于乘法的结果。 - Marius
6
@Marius:它并不是被丢弃,而是被覆盖。第二个++确实被执行了,并且确实将a增加到5...然后a的值被现在已经计算出来的赋值结果所取代。你使用相同的变量进行赋值并不会改变任何操作符的行为。 - Bobson
1
@Ben:Java和C++、C一样,都有前缀和后缀的自增和自减操作:http://java.sun.com/docs/books/jls/third_edition/html/expressions.html#15.15.1 - Samuel Edwin Ward
4
Java和C++的主要区别在于Java指定了更多的顺序点,因此像这样的代码实际上是可定义的,而不像C++那样,这段代码很容易被解释为任意数量的东西。 - fluffy
显示剩余2条评论

31

您的陈述:

a += (a++) * (a++);

等价于下列任何一个:

a = a*a + 2*a
a = a*(a+2)
a += a*(a+1)

可以使用其中任意一个代替。


5
为什么它返回12? - Robert Harvey
2
@RokKralj 这是一道理论考试题目,旨在极端且模糊。 :) - Madara's Ghost
1
Rok 应该写成“a becomes 4”而不是“a=4”,并且“a becomes 5”应该在“a=3*4”之前。 - Mike
第一句话加1分,包括这个问题的考试减1分。 - Andy Thomas
1
@Agos:我猜他认为首先计算3 * 3,然后赋值给a,然后a会增加两次--> 11。或者计算3 * 4,结果增加一次--> 13。虽然这没有多少意义,但是如果你看一些其他的前缀/后缀递增问题,你会感到“任何事情都有可能”... - Tim Pietzcker
显示剩余4条评论

25

a++ 的意思是 '取得a的数值, 然后将a增加1'。因此,当你运行以下代码时:

(a++) * (a++)

首先执行第一个a++,得到值3。然后a加1。接着执行第二个a++a产生值4,然后再次加1(但现在这没关系了)。

因此,最终结果为

a = 3 * 4

其值为12。


9
int a = 3;
a += (a++) * (a++);

首先构建语法树:

+=
  a
  *
    a++
    a++

为了评估它,从最外层元素开始递归下降。对于每个元素执行以下操作:
  • 从左到右评估子项
  • 评估元素本身
+=运算符是特殊的:它被扩展为类似于left = left + right的内容,但仅评估left表达式一次。尽管如此,在右侧得到值之前,左侧会被评估为一个值(而不仅仅是一个变量)。
这导致了以下结果:
  1. 开始评估+=
  2. 评估分配给变量a的赋值语句的左侧。
  3. 评估变量a以获得将用于加法的值3
  4. 开始评估*
  5. 评估第一个a++。这返回变量a的当前值3并将a设置为4
  6. 评估第二个a++。这返回变量a的当前值4并将a设置为5
  7. 计算乘积:3 * 4 = 12
  8. 执行+=。在第三步中,左侧已被评估为3,右侧为12。因此,它将3 + 12 = 15分配给a
  9. a的最终值是15。
这里需要注意的一件事是运算符优先级对评估顺序没有直接影响。它只影响树的形式,从而间接地影响顺序。但在树中的兄弟姐妹之间,无论运算符优先级如何,评估顺序始终是从左到右的。

1
如果运行代码,最终答案是15。 - Marius

7

(a++) 是后置自增运算符,因此表达式的值为3。

(a++) 是后置自增运算符,因此表达式的值现在为4。

表达式求值是从左到右进行的。

3 * 4 = 12 

2
3*4 = 12:嘿,这就是我所说的毋庸置疑的真理 XD。 - Mister Smith

7

每次使用a++时,你都会对a进行后增量操作。这意味着第一个a++的值为3,第二个a++的值为4。3 * 4 = 12。


5

人们普遍对操作符的工作方式缺乏理解。实际上,每个操作符都是语法糖。

你所需要做的就是理解每个操作符背后实际发生了什么。假设以下情况:

a = b -> Operators.set(a, b) //don't forget this returns b
a + b -> Operators.add(a, b)
a - b -> Operators.subtract(a, b)
a * b -> Operators.multiply(a, b)
a / b -> Operators.divide(a, b)

然后可以使用这些概括来重写复合运算符(为了简单起见,请忽略返回类型):

Operators.addTo(a, b) { //a += b
  return Operators.set(a, Operators.add(a, b));
}

Operators.preIncrement(a) { //++a
  return Operators.addTo(a, 1);
}

Operators.postIncrement(a) { //a++
  Operators.set(b, a);
  Operators.addTo(a, 1);
  return b;
}

您可以重写您的例子:
int a = 3;
a = (a++) * (a++);

作为

Operators.set(a, 3)
Operators.set(a, Operators.multiply(Operators.postIncrement(a), Operators.postIncrement(a)));

这可以使用多个变量进行拆分:

Operators.set(a, 3)
Operators.set(b, Operators.postIncrement(a))
Operators.set(c, Operators.postIncrement(a))
Operators.set(a, Operators.multiply(b, c))

这种写法确实更冗长一些,但很快就会明显地意识到,你永远不想在单行上执行超过两个操作。


5

在以下情况下:

int a = 3;  
a = (a++) * (a++); 

a = 3 * a++; now a is 4 because of post increment
a = 3 * 4; now a is 5 because of second post increment
a = 12; value of 5 is overwritten with 3*4 i.e. 12 

因此我们得到的输出是12。

在以下情况下:

a += (a++) * (a++); 
a = a + (a++) * (a++);
a = 3 + (a++) * (a++); // a is 3
a = 3 + 3 * (a++); //a is 4
a = 3 + 3 * 4; //a is 5
a = 15

这里需要注意的主要点是,编译器在这种情况下是从左往右解析的,对于后置递增运算符,计算时使用递增前的值,随着从左到右的移动,使用的是递增后的值。


3
这里是Java代码:
int a = 3;
a = (a++)*(a++);

以下是字节码:

  0  iconst_3
  1  istore_1 [a]
  2  iload_1 [a]
  3  iinc 1 1 [a]
  6  iload_1 [a]
  7  iinc 1 1 [a]
 10  imul
 11  istore_1 [a]

以下是发生的情况:

将3推入栈中,然后从栈中弹出3并将其存储在a中。 现在a = 3,栈为空。

  0  iconst_3
  1  istore_1 a

现在它将值从“a”(3)推入堆栈,然后将a增加1(3->4)。
  2  iload_1 [a]
  3  iinc 1 1 [a]

现在,“a”等于“4”,堆栈等于{3}。

然后它再次加载“a”(4),将其推入堆栈并递增“a”。

  6  iload_1 [a]
  7  iinc 1 1 [a]

现在,“a”等于5,堆栈等于{4,3}。
因此,它最终从堆栈中弹出前两个值(4和3),将它们相乘并将结果存回堆栈中(12)。
 10  imul

现在,“a”等于5,堆栈等于12。
最后从堆栈弹出12并存储在a中。
 11  istore_1 [a]

太棒了!


3

(a++) 表示返回 a 并且增加,因此 (a++) * (a++) 的意思是 3 * 4。


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