为什么 x == (x = y) 不同于 (x = y) == x?

212

考虑以下示例:

class Quirky {
    public static void main(String[] args) {
        int x = 1;
        int y = 3;

        System.out.println(x == (x = y)); // false
        x = 1; // reset
        System.out.println((x = y) == x); // true
     }
}

我不确定Java语言规范中是否有一个条款规定在与右侧(x = y)比较之前加载变量的先前值,这应该按括号所示的顺序计算。
为什么第一个表达式的结果为false,而第二个表达式的结果为true? 我本来期望(x = y)首先被评估,然后将比较x与自身(3)并返回true
这个问题与Java表达式中子表达式求值的顺序不同,因为这里的x绝对不是一个“子表达式”,它需要被加载以进行比较,而不是被“评估”。这个问题是针对Java的,表达式x == (x = y)不像许多为了棘手面试问题而精心设计的不切实际的结构,它来自于一个真实的项目。 它原本应该作为一个一行替换比较和替换习语的。
int oldX = x;
x = y;
return oldX == y;

Java中,由于比x86 CMPXCHG指令更简单,因此应该使用更短的表达方式。


63
左侧总是在右侧之前被计算。括号对此没有影响。 - Louis Wasserman
11
评估表达式 x = y 是非常相关的,会导致副作用,即将 x 设置为 y 的值。 - Louis Wasserman
51
请勿在对状态进行检查的同一行中混合状态改变的操作,这会极大地降低代码的可读性,请为自己和团队成员着想。(当需要满足原子性要求时可能存在必须这样做的情况,但是已经存在用于此目的的函数,并且它们的作用会立即被认出。) - jpmc26
50
真正的问题是为什么你想要写出这样的代码。 - klutt
26
你问题的关键在于你错误地认为括号意味着表达式的计算顺序。这是一个普遍的信仰,因为我们在小学数学和一些初学者编程书籍中的教学方式是这样的,但这是个错误的信仰。这是一个相当常见的问题。你可以通过阅读我关于该主题的文章获益; 它们是关于C#的,但同样适用于Java: https://ericlippert.com/2008/05/23/precedence-vs-associativity-vs-order/ https://ericlippert.com/2009/08/10/precedence-vs-order-redux/ - Eric Lippert
显示剩余22条评论
14个回答

164

43
“appears to be”这个措辞听起来似乎他们不太确定,说实话。 - Mr Lister
86
"appears to be" 的意思是规范并不要求操作按照时间顺序实际执行,但它要求你得到的结果与如果按照时间顺序执行操作所得到的结果相同。 - Robyn
25
@MrLister的说法中,“appears to be”似乎是一个不恰当的词语选择。他们所指的“appear”,实际上是指“在开发人员看来呈现为一种现象”。更好的短语可能是“is effectively”。 - Kelvin
18
在C++社区中,这相当于“仿佛”规则……操作数需要按照以下规则实现,即使从技术上讲并非如此,也要表现得“仿佛”是这样实现的。 - Michael Edenfield
2
@Kelvin 我同意,我也会选择那个词,而不是“似乎是”。 - MC Emperor
显示剩余3条评论

149

正如LouisWasserman所说,表达式从左到右进行评估。Java并不关心“评估”实际上是做什么,它只关心生成一个(非易失性、最终)值来使用。

//the example values
x = 1;
y = 3;

因此,要计算System.out.println()的第一个输出,需要执行以下操作:

x == (x = y)
1 == (x = y)
1 == (x = 3) //assign 3 to x, returns 3
1 == 3
false

并计算第二个:

(x = y) == x
(x = 3) == x //assign 3 to x, returns 3
3 == x
3 == 3
true

请注意,第二个值始终将评估为 true,无论xy的初始值如何,因为您实际上正在比较将值分配给其分配的变量,并且按定义顺序评估的a = bb将始终相同。


顺序从左到右在数学中也是正确的,当你遇到括号或优先级时,你需要在其中迭代并从左到右评估主层内的所有内容,然后再继续进行。但是数学永远不会这样做;区别只有在这不是一个方程而是一个组合操作,一次完成赋值和方程时才有意义。我永远不会这样做,因为可读性很差,除非我在进行代码高尔夫或寻找优化性能的方法,那么就会有注释。 - Harper - Reinstate Monica

98

根据括号所示的顺序应首先计算

不是的。普遍误解括号对计算或评估顺序有任何影响。它们只是将表达式的部分强制转换为特定的树形结构,将合适的操作绑定到合适的右操作数上。

(如果您不使用它们,这些信息来自运算符的“优先级”和关联性,这是语言语法树定义的结果。事实上,即使您使用括号,它仍然完全按照这种方式工作,但是我们简化并说我们不依赖任何优先规则。)

一旦完成这个过程(即代码已经被解析为程序)那么操作数仍然需要进行评估,有关如何执行评估的单独规则:Java中,每个操作的左操作数都会首先进行评估(正如Andrew所展示的)。

请注意,这不是所有语言的情况;例如,在C ++中,除非您使用像&&||这样的短路运算符,否则操作数的计算顺序通常未指定,您不应该依赖它的任何方式。

老师们需要停止使用误导性的短语,例如“这会使加法先发生”来解释运算符优先级。对于表达式x * y + z,正确的解释应该是“运算符优先级使得加法在x * yz之间发生,而不是在yz之间发生”,没有提到任何“顺序”。


6
我希望我的老师们在教授数学时,能够将其基础知识和代表它的语法进行分开。比如花一天的时间学习罗马数字或波兰表示法等,从而发现加法具有相同的属性。我们在中学已经学过结合律等属性,所以有足够的时间来进行这方面的教学。 - John P
1
很高兴你提到这个规则并不适用于所有编程语言。此外,如果任一方有其他副作用,比如写入文件或读取当前时间,在Java中甚至是未定义的顺序。然而,比较的结果将会像从左到右进行评估一样(在Java中)。另外一点:相当多的编程语言通过语法规则简单地禁止了混合赋值和比较的方式,因此这个问题就不会出现。 - Abel
5
@JohnP:情况变得更糟了。5*4 是指 5+5+5+5 还是 4+4+4+4+4?一些老师坚持认为只有其中一种选择是正确的。 - Brian
3
但是...但是...实数的乘法是可交换的! - Lightness Races in Orbit
2
在我的思维世界中,一对括号代表“需要”。计算a*(b+c)时,括号表明加法的结果“是乘法所需的”。任何隐式的运算符优先级都可以用括号表示,除了左右顺序规则。(这是真的吗?)@Brian在数学中有几种罕见情况下可以用重复加法代替乘法,但这远非总是正确的(从复数开始但不限于此)。因此你们的教育工作者应该非常注意他们向人们传达的内容... - syck
显示剩余4条评论

25

我不确定Java语言规范中是否有一条规定加载变量的先前值...

有。下次如果您不确定规范的内容,请先阅读规范,然后如果仍不清楚再提出问题。

...右侧的表达式 (x = y),根据括号的顺序应该首先计算。

这个说法是错误的。括号不意味着计算顺序。在Java中,计算顺序从左向右,与括号无关。括号只决定子表达式的边界,而不是计算顺序。

为什么第一个表达式的结果是 false,但第二个表达式的结果是 true?

== 运算符的规则是:先计算左侧的表达式,得到一个值;再计算右侧的表达式,得到另一个值;然后比较这两个值,比较的结果就是整个表达式的值。

换句话说,expr1 == expr2 的意思永远与写成 temp1 = expr1; temp2 = expr2; 然后计算 temp1 == temp2 是一样的。

对于左侧为局部变量的 = 运算符,规则是:先计算左侧表达式得到一个变量;再计算右侧表达式得到一个值;然后执行赋值操作,结果是被赋的那个值。

把这些规则放在一起:

x == (x = y)
我们有一个比较运算符。先评估左侧以产生一个值——我们得到x的当前值。然后评估右侧:这是一个赋值,所以我们评估左侧以产生一个变量——变量x——然后我们评估右侧——y的当前值——将其分配给x,结果是分配的值。然后我们将x的原始值与分配的值进行比较。 您可以尝试使用 (x = y) == x 进行练习。再次记住,在评估右侧规则之前,所有评估左侧的规则都会发生。 您的期望是基于一组关于Java规则的错误信念。希望现在您有正确的信念,并且将来会期望真实的事情。 这个问题不同于“Java表达式中子表达式的评估顺序”。 这个说法是错误的。那个问题完全相关。 x在这里绝对不是一个'subexpression'。 这个说法也是错误的。它在每个示例中都是一个子表达式两次。 需要加载它进行比较而不是进行'评估'。 我不知道这是什么意思。 显然,您仍然有许多错误的信念。我的建议是,您阅读规范,直到您的错误信念被真实信念取代。 这个问题是特定于Java的,表达式x == (x = y)与为棘手的面试问题而创造的遥不可及、不切实际的构造物不同,它源自一个真实的项目。 表达式的来源与问题无关。这样的表达式的规则在规范中清楚地描述了;请阅读它! 顺便说一句,C#有一个库方法compare and replace,它可以编译成机器指令。我相信Java没有这样的方法,因为它不能表示在Java类型系统中。

8
如果任何人都能阅读完整的JLS,那么就没有出版Java书籍的必要了,至少这个网站的一半也将变得无用。 - John McClane
8
@JohnMcClane:我向你保证,阅读整个Java规范没有任何困难,但是你也不必这样做。Java规范以一个有用的“目录”开头,可以帮助你快速找到你最感兴趣的部分。它也可以在线搜索关键字。话虽如此,你说得对:有很多好资源可以帮助你了解Java的工作原理;我的建议是让你利用它们! - Eric Lippert
8
这个回答没有必要表现得傲慢和无礼。请记住:保持友善 - walen
7
@LuisG.: 没有任何轻蔑的意思;我们在这里都是互相学习的,我不会推荐我自己作为初学者没有尝试过的事情。这也并不是无礼的行为。明确无误地指出原帖作者的谬论是一种善意。隐藏在“礼貌”之后,让人们继续持有错误的信念是没用的,会强化错误的思维习惯 - Eric Lippert
5
@LuisG.:我曾经写过一篇关于JavaScript设计的博客,而最有帮助的评论来自Brendan。他清晰明确地指出了我的错误,并且我非常感激他花时间指正我。因为这个经历,我在接下来的20年里没有在自己的工作中重复那个错误,更没有将它教给别人。同时,这也让我有机会通过以自己为例子展示人们如何相信错误的事情来纠正别人相同的错误信念。 - Eric Lippert
显示剩余7条评论

16

这与运算符优先级和运算符如何被评估有关。

括号“()”具有更高的优先级,其结合性是从左到右的。 相等性“==”在此问题中排在其后,并且其结合性是从左到右的。 赋值“=”最后出现,其结合性是从右到左的。

系统使用堆栈来评估表达式。 表达式从左到右进行评估。

现在转到原始问题:

int x = 1;
int y = 3;
System.out.println(x == (x = y)); // false

首先 x(1) 将被推入堆栈。 然后将计算内部的 (x = y) 并将其与值为 x(3) 的元素一起推入堆栈。 现在将 x(1) 与 x(3) 进行比较,结果为 false。

x = 1; // reset
System.out.println((x = y) == x); // true

在这里,(x = y)将被评估,现在x的值变为3并且x(3)将被推入堆栈。现在具有更改后的值的x(3)将被推入堆栈。现在表达式将被评估,并且两者将相同,因此结果为true。


12

不一样。左侧的表达式总是在右侧之前被计算,括号并没有指定执行顺序,而是将命令分组。

例如:

      x == (x = y)

你基本上正在做的与以下相同:

      x == y

在比较后,x 的值将会与 y 相同。

而使用:

      (x = y) == x

你基本上正在做与以下相同的事情:

      x == x

x接管了y的值之后,它将始终返回true


9

第一个测试是检查1是否等于3。

第二个测试是检查3是否等于3。

(x = y) 分配了值,并测试该值。在前面的例子中,首先 x = 1,然后 x 被赋值为 3。1 == 3 吗?

在后者中,x 被赋值为 3,显然它仍然是 3。3 == 3 吗?


8

考虑这个也许更简单的例子:

int x = 1;
System.out.println(x == ++x); // false
x = 1; // reset
System.out.println(++x == x); // true

在这里,++x 中的预增运算符必须在比较之前应用——就像您的示例中的 (x = y) 必须在比较之前计算。

但是,表达式求值仍然从左往右进行,因此第一个比较实际上是 1 == 2,而第二个比较是 2 == 2
您的示例中也是同样的情况。


8

表达式从左到右进行计算。在这种情况下:

int x = 1;
int y = 3;

x == (x = y)) // false
x ==    t

- left x = 1
- let t = (x = y) => x = 3
- x == (x = y)
  x == t
  1 == 3 //false

(x = y) == x); // true
   t    == x

- left (x = y) => x = 3
           t    =      3 
-  (x = y) == x
-     t    == x
-     3    == 3 //true

5

基本上,第一个语句x的值为1,因此Java将1与新的x变量进行比较,这两个值不同。

在第二个语句中,你说x = y,这意味着x的值已更改,因此当你再次调用它时,它将是相同的值,因此它为真,x == x。


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