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


207

考虑以下示例:

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;

它比x86 CMPXCHG指令还要简单,因此值得在Java中使用较短的表达式。


62
总是先评估左侧,然后评估右侧。方括号对此没有影响。
Louis Wasserman '18

11
评估表达式x = y当然是相关的,并且会导致将副作用x设置为的值y
Louis Wasserman '18

50
给自己和队友一个忙,不要将状态突变与状态检查混为一谈。这样做会大大降低代码的可读性。(在某些情况下,由于原子性要求绝对必要,但这些功能已经存在,其目的将被立即识别。)
jpmc26

50
真正的问题是为什么要编写这样的代码。
klutt

26
问题的关键是您对括号表示评估顺序的错误信念。这是一种普遍的信念,这是因为我们在小学时就教过数学,并且因为一些初学者的编程书籍仍然弄错了它,但这是错误的信念。这是一个非常常见的问题。您可能会受益于阅读我关于该主题的文章;它们与C#有关,但它们适用于Java:ericlippert.com/2008/05/23/precedence-vs-associativity-vs-order ericlippert.com/2009/08/10/precedence-vs-order-redux
Eric Lippert

Answers:


97

应按括号中的顺序先计算

不会。圆括号对计算或评估顺序有任何(一般)影响,这是一个普遍的误解。它们仅将表达式的部分强制转换为特定的树,将正确的操作数绑定到作业的正确操作。

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

一旦完成(即,将您的代码解析为程序),仍然需要对那些操作数进行求值,并且有关于如何完成操作的单独规则:所述规则(如安德鲁向我们展示的那样)指出每个操作的LHS首先用Java评估。

请注意,并非所有语言都如此。例如,在C ++中,除非您使用短路运算符(如&&或)||,否则操作数的评估顺序通常是不确定的,并且您不应以任何方式依赖它。

教师需要停止使用误导性短语来解释操作员优先级,例如“这使添加操作首先发生”。给定一个表达式x * y + z,正确的解释是“运算符优先级使x * yz之间发生加法,而不是y和之间发生加法,而z没有提及任何”顺序”。


6
我希望我的老师在基础数学和他们用来表示数学的语法之间进行了区分,例如,如果我们花一天时间用罗马数字或波兰符号或其他东西看得出该加法具有相同的属性。我们在中学时就学会了联想能力和所有这些特性,因此有很多时间。
约翰·P

1
很高兴您提到此规则并非对所有语言都适用。同样,如果任何一方都有另一种副作用,例如写入文件或读取当前时间,则它(即使在Java中)的发生顺序也是不确定的。但是,比较的结果就像是从左到右进行评估(在Java中)。撇开另一个:许多语言只是不允许通过语法规则以这种方式混合分配和比较,并且不会出现此问题。
亚伯

5
@JohnP:情况变得更糟。5 * 4表示5 + 5 + 5 + 5还是4 + 4 + 4 + 4 + 4?一些老师坚持认为,这些选择中只有一种是正确的。
布赖恩

3
@Brian但是...但是...实数的乘积是可交换的!
Lightness Races in Orbit Race '18

2
在我的思维世界中,一对括号代表“需要”。计算“ a *(b + c)”时,括号将表示相乘的结果乘法所必需的。 LHS-first或RHS-first规则,任何隐式运算符首选项都可以用括号表示。(是真的?)@Brian在数学中,有少数情况下可以用重复加法来代替乘法,但到目前为止并非总是如此(从复数开始但不限于此)。所以,你的教育应该真正对什么都告诉人们的眼睛....
SYCK

164

==是二进制相等运算符

评估右侧操作数的任何部分之前,似乎已对二进制运算符的左侧操作数进行了完全评估。

Java 11规范>评估顺序>首先评估左手操作数


42
“似乎是”一词听起来并不像他们确定的那样,tbh。
李斯特先生

86
“似乎是”表示规范不要求按时间顺序实际执行该操作,而是要求您获得与操作相同的结果。
罗宾

24
@MrLister“似乎是”似乎对他们而言是一个糟糕的选择。“出现”是指“对开发人员来说是一种现象”。“有效”可能是一个更好的说法。
开尔文

18
在C ++社区中,这等效于“假设”规则...操作数必须表现为“好像”是按照以下规则实现的,即使在技术上不是这样。
Michael Edenfield '18

2
@Kelvin我同意,我会选择那个词,而不是“似乎是”。
MC Emperor

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会以该顺序进行评估,始终是相同的根据定义。


顺便说一下,顺便说一句,从左到右在数学上也是正确的,就是当您到达括号或优先级时,您要在它们内部进行迭代并评估从左到右的所有内容,然后再进一步进行主层运算。但是数学永远不会做到这一点。区别仅是重要的,因为这不是方程式,而是组合操作,可以一次完成赋值和方程式。我永远不会这样做,因为可读性很差,除非我正在做代码高尔夫或正在寻找一种优化性能的方法,然后会有评论。
哈珀-恢复莫妮卡

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作为练习来做。同样,请记住,所有评估左侧的规则都发生在所有评估右侧的规则之前

我本来希望(x = y)首先被评估,然后它将x与自身(3)进行比较并返回true。

您的期望是基于对Java规则的一系列错误信念。希望您现在拥有正确的信念,并且将来会期待真实的事情。

这个问题不同于“ Java表达式中子表达式的求值顺序”

这句话是错误的。这个问题完全贴切。

x绝对不是此处的“子表达式”。

这句话也是错误的。在每个示例中,它都是一个子表达式两次

需要加载它以进行比较,而不是对其进行“评估”。

我不知道这是什么意思。

显然,您仍然有许多错误的信念。我的建议是您阅读规范,直到您的错误信念被真实信念取代。

这个问题是特定于Java的,并且表达式x ==(x = y)与通常为棘手的面试问题设计的牵强的不切实际的构造不同,它来自一个真实的项目。

该表述的出处与该问题无关。在说明书中清楚地描述了这种表达的规则。阅读!

它本来是用于替换和替换成语的单行替换

由于单行替换在您(代码的阅读者)中引起了很大的混乱,我建议这是一个糟糕的选择。使代码更简洁但更难理解并不是胜利。不太可能使代码更快。

顺便说一句,C#将比较和替换作为库方法,可以将其简化为机器指令。我相信Java没有这种方法,因为它不能在Java类型系统中表示。


8
如果任何人都可以阅读整个JLS,那么就没有理由出版Java书籍,而且该站点的至少一半也将毫无用处。
John McClane

8
@JohnMcClane:我向您保证,遍历整个规范没有任何困难,但是您也不必这样做。Java规范从一个有用的“目录”开始,它将帮助您快速进入您最感兴趣的部分。它也可以在线和通过关键字搜索。就是说,您是正确的:有很多好的资源可以帮助您学习Java的工作原理;我对您的建议是您可以利用它们!
埃里克·利珀特

8
这个答案是不必要的屈从和无礼。记住:要友善
沃伦

7
@LuisG .:无意或暗中屈尊;我们都在这里互相学习,我不建议初学者时做任何自己没有做的事情。也不粗鲁。清楚明确地确定他们的错误信念是对原始海报的一种友善。躲在“礼貌”之后,让人们继续有错误的信念是无济于事的,并加剧了不良的思想习惯
埃里克·利珀特

5
@LuisG .:我曾经写过一个关于JavaScript设计的博客,而我得到的最有用的评论来自Brendan,它清楚明确地指出了我弄错了地方。真是太好了,我感谢他抽出宝贵的时间,因为我接下来的20年没有再在工作中重蹈覆辙,或更糟糕的是,把它教给了别人。这也给了我机会,以自己为例,人们逐渐相信了错误的事物,从而纠正了对他人同样错误的信念。
埃里克·利珀特

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将价值ÿ比较之后。

同时使用:

      (x = y) == x

基本上,您的操作与:

      x == x

x 之后取y的值。并且它将始终返回true


9

在第一个测试中,您要检查1 == 3。

在第二个测试中,您的检查是3 == 3。

(x = y)分配值,并测试该值。在前面的示例中,首先x = 1,然后分配x。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必须在进行比较之前应用pre-increment运算符-就像(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的值已更改,因此当您再次调用它时,它将是相同的值,因此为什么它为true并且x == x


4

==是一个比较相等运算符,它从左到右起作用。

x == (x = y);

这里将x的旧分配值与x的新分配值进行比较(1 == 3)// false

(x = y) == x;

而在这里,x的新赋值与比较之前分配给它的x的新保持值进行比较,(3 == 3)// true

现在考虑一下

    System.out.println((8 + (5 * 6)) * 9);
    System.out.println(8 + (5 * 6) * 9);
    System.out.println((8 + 5) * 6 * 9);
    System.out.println((8 + (5) * 6) * 9);
    System.out.println(8 + 5 * 6 * 9);

输出:

342

278

702

342

278

因此,括号仅在算术表达式中发挥其主要作用,而在比较表达式中则不起作用。


1
结论是错误的。算术运算符和比较运算符之间的行为没有区别。x + (x = y)并且(x = y) + x使用比较运算符会表现出与原始行为相似的行为。
JJJ

1
@JJJ在x +(x = y)和(x = y)+ x中不涉及比较,它只是将y值分配给x并将其添加到x。
Nisrin Dhoondia

1
...是的,这就是重点。错误的是,“括号仅在算术表达式中才起主要作用”,这是错误的,因为算术表达式和比较表达式之间没有区别。
JJJ

2

这里的东西是arithmatic运营商/关系运算符precedency为了出这两家运营商的=VS ==优势之一是==(关系运算符占主导地位),因为它先=赋值运算符。尽管具有优先级,但评估顺序为LTR(从左到右),评估顺序后优先级出现。因此,无论约束评估是什么,LTR都是如此。


1
答案是错误的。运算符优先级不会影响评估顺序。阅读一些投票最多的答案以作解释,尤其是这个答案。
JJJ

1
正确,实际上这是我们被教给优先级限制的幻觉的方式,但所有这些都正确地指出,它没有影响,因为评估顺序仍然是从左到右
Himanshu Ahuja

-1

很容易在左边的第二个比较中,在将y赋给x之后(左边),然后比较3 == 3。总是从x的左到右采用当前状态读取语句。


-2

如果您要编写Java编译器或测试程序以验证Java编译器是否正常运行,那么您提出的问题是一个很好的问题。在Java中,这两个表达式必须产生您看到的结果。例如,在C ++中,它们不必这样做-因此,如果有人在其Java编译器中重用了C ++编译器的某些部分,则从理论上讲,您可能会发现该编译器的运行不正常。

作为软件开发人员,编写可读性,可理解性和可维护性的代码,这两个版本的代码都将被认为很糟糕。要了解代码的作用,必须确切地知道Java语言是如何定义的。既写Java代码又写C ++代码的人会不寒而栗。如果您必须问为什么一行代码可以完成它,那么您应该避免使用该代码。(我想并希望那些正确回答了“为什么”问题的人也会避免使用该代码)。


“要了解代码的作用,必须确切地知道Java语言是如何定义的。” 但是,如果每个同事都认为这是常识怎么办?
BinaryTreeee
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.