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

Why is x == (x = y) not the same as (x = y) == x?

考虑以下示例:

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) 和 return true.

进行比较

这道题和不同的是x这里绝对不是'subexpression'。比较需要 loaded 而不是 'evaluated'。这个问题是 Java 特定的,表达式 x == (x = y) 与通常为棘手的面试问题精心设计的牵强不切实际的结构不同,它来自一个真实的项目。它应该是比较和替换成语的单行替换

int oldX = x;
x = y;
return oldX == y;

它比 x86 CMPXCHG 指令更简单,应该在 Java.

中使用更短的表达式

正如 LouisWasserman 所说,表达式是从左到右求值的。 java 不关心 "evaluate" 实际做了什么,它只关心生成一个(非易失性,最终)值来处理。

//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

请注意,无论 xy 的初始值如何,第二个值的计算结果始终为真,因为您实际上是在将值的赋值与其赋值的变量进行比较到,并且 a = bb 将按该顺序计算,根据定义始终相同。

== 是二进制 equality operator.

The left-hand operand of a binary operator appears to be fully evaluated before any part of the right-hand operand is evaluated.

Java 11 Specification > Evaluation Order > Evaluate Left-Hand Operand First

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

在第二个测试中,您的检查结果为 3 == 3。

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

后者x赋值为3,明明还是3,3 == 3吗?

which, by the order implied by brackets, should be calculated first

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

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

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

中评估

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

教师需要停止使用 "this makes the addition happen first" 等误导性短语来解释运算符优先级。给定一个表达式 x * y + z 正确的解释是 "operator precedence makes the addition happen between x * y and z, rather than between y and z",没有提及任何 "order".

这与运算符优先级以及运算符的计算方式有关。

圆括号'()'具有更高的优先级并且具有从左到右的结合性。 等式 '==' 在这个问题中紧随其后,并且从左到右具有关联性。 赋值“=”排在最后并且从右到左具有关联性。

系统使用堆栈计算表达式。表达式从左到右求值。

现在回到原来的问题:

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

首先x(1)将被压入堆栈。 然后 inner (x = y) 将被评估并与值 x(3) 一起被推入堆栈。 现在 x(1) 将与 x(3) 进行比较,因此结果为假。

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

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

不一样。左边总是先于右边计算,括号中没有指定执行顺序,而是一组命令。

有:

      x == (x = y)

你基本上是在做同样的事情:

      x == y

x比较后的值为y

同时:

      (x = y) == x

你基本上是在做同样的事情:

      x == x

x 取了 y 的值之后。它总是 return true.

基本上第一个语句 x 的值为 1 所以 Java 将 1 == 与新的 x 变量进行比较,后者将不相同

在第二个中你说 x=y 这意味着 x 的值改变了所以当你再次调用它时它会是相同的值因此它是真的并且 x ==x

考虑另一个可能更简单的例子:

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
同样的事情发生在你的例子中。

表达式从左到右求值。在这种情况下:

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

I'm not sure if there is an item in the Java Language Specification that dictates loading the previous value of a variable...

有。下次您不清楚规范说的是什么,请阅读规范,然后然后如果不清楚,请提出问题。

... the right side (x = y) which, by the order implied by brackets, should be calculated first.

这个说法是错误的。 括号并不表示求值顺序。在 Java 中,求值顺序是从左到右,与括号无关。括号决定子表达式边界的位置,而不是求值顺序。

Why does the first expression evaluate to false, but the second evaluate to true?

==运算符的规则是:计算左边产生一个值,计算右边产生一个值,比较值,比较的是表达式的值。

换句话说,expr1 == expr2 的含义始终与您编写 temp1 = expr1; temp2 = expr2; 然后计算 temp1 == temp2.

的含义相同

左边有局部变量的=运算符的规则是:计算左边产生一个变量,计算右边产生一个值,进行赋值,结果为分配的值。

所以放在一起:

x == (x = y)

我们有一个比较运算符。评估左侧以产生一个值——我们得到 x 的当前值。评估右侧:这是一个赋值,所以我们评估左侧以产生一个变量 - 变量 x - 我们评估右侧 - y 的当前值 - 将其分配给x,结果就是赋值。然后我们将 x 的原始值与分配的值进行比较。

你可以做 (x = y) == x 作为练习。再次记住,所有评估左侧的规则发生在所有评估右侧的规则之前

I would have expected (x = y) to be evaluated first, and then it would compare x with itself (3) and return true.

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

This question is different from "order of evaluation of subexpressions in a Java expression"

这个说法是错误的。这个问题是完全相关的。

x is definitely not a 'subexpression' here.

这个说法也是假的。它是每个示例中的子表达式两次

It needs to be loaded for the comparison rather than to be 'evaluated'.

我不知道这是什么意思。

看来你还有很多错误的信念。我的建议是您阅读规范,直到您的错误信念被真实信念取代。

The question is Java-specific and the expression x == (x = y), unlike far-fetched impractical constructs commonly crafted for tricky interview questions, came from a real project.

表达式的出处与问题无关。规范中清楚地描述了此类表达式的规则;阅读它!

It was supposed to be a one-line replacement for the compare-and-replace idiom

由于单行替换对代码 reader 造成了很大的混乱,我认为这是一个糟糕的选择。使代码更简洁但更难理解并不是胜利。不太可能使代码更快。

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

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

作为软件开发人员,编写可读、可理解和可维护的代码,您的代码的两个版本都会被认为很糟糕。要了解代码的作用,必须确切了解Java语言是如何定义的。同时编写 Java 和 C++ 代码的人看到代码会不寒而栗。如果你不得不问为什么一行代码做了它所做的事情,那么你应该避免使用该代码。 (我想并希望正确回答您 "why" 问题的人自己也能避免使用该代码)。

在左边的第二个比较中很容易是在将 y 分配给 x(在左边)之后进行赋值,然后比较 3 == 3。在第一个示例中,您将 x = 1 与新分配 x = 进行比较3. x的从左到右好像一直有taken current state reading statements.

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

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);

Output:

342

278

702

342

278

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

这里的事情是两个运算符 === 中的算术 operators/relational 运算符优先顺序,占主导地位的是 == (关系运算符占主导地位)作为它在 = 赋值运算符之前。 尽管有优先级,但评估顺序是 LTR(从左到右)优先级在评估顺序之后出现。 因此,无论任何约束评估都是 LTR。