为什么x == (x = y)和(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)并返回true


这个问题与Java表达式中子表达式的求值顺序不同,因为x在这里绝对不是“子表达式”。它需要加载进行比较,而不是被“求值”。这个问题是java特有的,表达式x == (x = y)不同于通常为棘手的面试问题而设计的牵强的不切实际的构造,它来自一个真实的项目。它应该是比较-替换习惯用法的一行替换

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

它比x86的CMPXCHG指令更简单,在Java中应该有更短的表达式。

19582 次浏览

正如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的初始值如何,第二个值的值总是为true,因为你实际上是在将值的赋值与赋值的变量进行比较,并且根据定义,a = bb的赋值顺序总是相同的。

==是一个二进制相等操作符

二进制操作符的左操作数似乎是完全求值的。

Java 11规范>计算顺序>计算左操作数优先

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

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

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

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

根据括号所示的顺序,哪个应该先计算

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

(而且,如果不使用它们,这些信息来自操作符的“优先级”和结合性,这是语言语法树定义的结果。事实上,当你使用圆括号时,它仍然是这样工作的,但我们简化了,说我们不依赖于任何优先规则。)

一旦完成了这一点(即一旦你的代码被解析成一个程序),这些操作数仍然需要被求值,并且有关于如何做的单独的规则:所说的规则(正如Andrew向我们展示的那样)声明每个操作的LHS在Java中首先被求值。

注意,并非所有语言都是如此;例如,在c++中,除非你使用的是类似&&||这样的短路操作符,否则操作数的求值顺序通常是未指定的,你不应该依赖于它。

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

它与操作符优先级以及如何计算操作符有关。

括号'()'优先级更高,且具有从左到右的结合性。 等式'=='在这个问题中是下一个,从左到右具有结合律。 赋值'='放在最后,具有从右到左的结合性

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

现在回到最初的问题:

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
< p >, (x = y)将被计算,现在x值变为3,x(3)将被推入堆栈。 现在x(3)的值改变后的等式将被推入堆栈。 现在表达式将被求值,两者将相同,因此结果为真

这是不一样的。左边总是在右边之前求值,括号没有指定执行顺序,而是指定命令的分组。

< p >:

      x == (x = y)

你所做的基本上是一样的:

      x == y

在比较之后,x将具有y的值。

< p >时:

      (x = y) == x

你所做的基本上是一样的:

      x == x

x取了y的值之后。并且它总是返回真正的.

基本上第一个语句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

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

有。下次当你不清楚规范说了什么时,请阅读规范并然后如果不清楚就问问题。

... 右边(x = y),根据括号所示的顺序,应该首先计算。

这种说法是错误的。括号并不表示求值的顺序。在Java中,求值的顺序是从左到右,不管括号是什么。括号决定子表达式的边界,而不是求值的顺序。

为什么第一个表达式的值为假,而第二个表达式的值为真?

==操作符的规则是:左边求值产生一个值,右边求值产生一个值,比较值,比较值就是表达式的值。

换句话说,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类型系统中表示。

如果您想编写Java编译器,或者测试程序以验证Java编译器是否正常工作,那么您提出的这类问题是一个非常好的问题。在Java中,这两个表达式必须产生您所看到的结果。例如,在c++中,它们就不需要这样做——所以如果有人在他们的Java编译器中重用了c++编译器的某些部分,理论上你可能会发现编译器没有像它应该的那样工作。

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

左边的第二个比较很简单,在把y赋值给x之后(在左边),你就比较3 == 3。在第一个例子中,你比较x = 1和新赋值x = 3。似乎总有一个当前状态从左到右读取x的语句。

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

x == (x = y);

这里x的旧赋值与x的新赋值进行比较,(1==3)//false

(x = y) == 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

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

这里的事情是算术操作符/关系操作符在两个操作符===中的优先顺序,占主导地位的是==(关系操作符占主导地位),因为它在=赋值操作符之前。 不管优先级,求值的顺序是LTR(从左到右)优先级在求值顺序之后。 因此,不管任何约束,求值都是LTR。