【问题标题】:Why is x == (x = y) not the same as (x = y) == x?为什么 x == (x = y) 与 (x = y) == x 不一样?
【发布时间】:2019-05-13 23:03:17
【问题描述】:

考虑以下示例:

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


这个问题与order of evaluation of subexpressions in a Java expression 不同,x 在这里绝对不是“子表达式”。它需要加载进行比较,而不是“评估”。这个问题是特定于 Java 的,表达式 x == (x = y) 不同于通常为棘手的面试问题而设计的牵强附会的不切实际的结构,它来自一个真实的项目。它应该是比较和替换习语的单行替换

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

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

【问题讨论】:

  • 左手边总是在右手边之前被评估。括号对此没有影响。
  • 评估表达式x = y 肯定是相关的,并导致将x 设置为y 的值的副作用。
  • 帮自己和队友一个忙,不要将状态突变与状态检查混为一谈。这样做会大大降低代码的可读性。 (在某些情况下,由于原子性要求,它是绝对必要的,但那些功能已经存在并且它们的目的将立即被识别。)
  • 真正的问题是你为什么要写这样的代码。
  • 您问题的关键是您错误地认为括号意味着评估顺序。这是一个普遍的信念,因为我们在小学的数学教学方式以及一些初学者编程书籍仍然会出错,但这是一个错误的信念。这是一个非常常见的问题。您可能会从阅读我关于该主题的文章中受益;它们是关于 C# 但它们适用于 Java:ericlippert.com/2008/05/23/precedence-vs-associativity-vs-orderericlippert.com/2009/08/10/precedence-vs-order-redux

标签: java variable-assignment equality operator-precedence jls


【解决方案1】:

== 是二进制 equality operator

二元运算符的左侧操作数似乎在右侧操作数的任何部分被评估之前已被完全评估。

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

【讨论】:

  • “似乎是”这个词听起来不像他们确定的那样,tbh。
  • “似乎是”表示规范不要求操作实际上按时间顺序执行,但它要求您获得与如果执行时相同的结果。跨度>
  • @MrLister “似乎是”对他们来说似乎是一个糟糕的词选择。 “出现”的意思是“向开发人员显示为一种现象”。 “有效地”可能是一个更好的短语。
  • 在 C++ 社区中,这等同于“as-if”规则......操作数必须表现得“好像”它是按照以下规则实现的,即使在技术上它是不是。
  • @Kelvin 我同意,我也会选择这个词,而不是“似乎是”。
【解决方案2】:

正如 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

请注意,无论xy 的初始值如何,第二个值都将始终计算为真,因为您实际上是将值的分配与其分配给它的变量进行比较,以及a = b并且b 将按照该顺序进行评估,根据定义始终相同。

【讨论】:

  • “从左到右”在数学中也是正确的,顺便说一下,当你到达括号或优先级时,你在它们内部迭代并从左到右评估所有内容,然后再继续主要层。但是数学永远不会这样做。唯一的区别是因为这不是一个方程,而是一个组合运算,可以一次性完成赋值和方程。我永远不会这样做,因为可读性很差,除非我在做代码高尔夫或正在寻找一种优化性能的方法,然后就会有 cmets。
【解决方案3】:

按照括号中的顺序,应该先计算

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

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

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

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

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

【讨论】:

  • 我希望我的老师们在基础数学和他们用来表示它的语法之间做出一些区分,就像我们花了一天时间使用罗马数字或波兰符号或其他任何东西并看到加法具有相同的特性。我们在中学学习了关联性和所有这些属性,所以时间充裕。
  • 很高兴您提到此规则不适用于所有语言。此外,如果任一方有另一个副作用,例如写入文件或读取当前时间,则(即使在 Java 中)未定义发生的顺序。但是,比较的结果就像是从左到右(在 Java 中)计算的。另一个问题:相当多的语言根本不允许通过语法规则以这种方式混合赋值和比较,并且不会出现问题。
  • @JohnP:情况变得更糟。 5*4 是指 5+5+5+5 还是 4+4+4+4+4 ?一些老师坚持认为只有一个选择是正确的。
  • @Brian 但是...但是...实数的乘法是可交换的!
  • 在我的思维世界里,一对括号代表“需要”。计算‘a*(b+c)’,括号将表示加法的结果是乘法所需要的。任何隐式运算符偏好都可以通过括号、except LHS-first 或 RHS-first 规则来表达。 (这是真的吗?)@Brian 在数学中有一些罕见的情况,乘法可以用重复加法代替,但到目前为止并不总是正确的(从复数开始但不限于)。因此,您的教育工作者应该真的关注他们告诉人们的内容......
【解决方案4】:

我不确定 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类型系统中表示。

【讨论】:

  • 如果有人可以浏览整个 JLS,那么就没有理由出版 Java 书籍了,而且这个网站至少有一半也没有用。
  • @JohnMcClane:我向你保证,通读整个规范没有任何困难,而且你也不必这样做。 Java 规范从一个有用的“目录”开始,它可以帮助您快速找到您最感兴趣的部分。它也是在线和关键字可搜索的。也就是说,你是对的:有很多很好的资源可以帮助你了解 Java 的工作原理;我对你的建议是你要利用它们!
  • 这个答案不必要地居高临下和粗鲁。记住:be nice.
  • @LuisG.: 没有贬低的意图或暗示;我们都在这里互相学习,我不推荐我在初学者时没有做过的任何事情。也不粗鲁。 清楚明确地识别他们的错误信念是对原始海报的善意。隐藏在“礼貌”背后,让人们继续持有错误的信念是无益的,并且会强化不良的思想习惯
  • @LuisG.:我曾经写过一篇关于 JavaScript 设计的博客,我得到的最有帮助的 cmets 来自 Brendan,他清楚明确地指出了我哪里出错了。那太好了,我感谢他抽出时间,因为我在接下来的 20 年里都没有在自己的工作中重复这个错误,或者更糟糕的是,把它教给别人。它还让我有机会以自己作为人们如何相信错误事物的例子来纠正对他人的同样错误信念。
【解决方案5】:

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

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

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

现在回到原来的问题:

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) 在相等后具有更改的值将被推入堆栈。 现在将评估表达式,并且两者都将相同,因此结果为真。

【讨论】:

    【解决方案6】:

    不一样。左侧总是在右侧之前被评估,并且括号不指定执行顺序,而是一组命令。

    有:

          x == (x = y)
    

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

          x == y
    

    x经过比较后会有y的值。

    同时:

          (x = y) == x
    

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

          x == x
    

    xy 的值之后。它总是会返回 true

    【讨论】:

      【解决方案7】:

      在您检查的第一个测试中,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
        同样的事情发生在你的例子中。

        【讨论】:

          【解决方案9】:

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

          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
          

          【讨论】:

            【解决方案10】:

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

            在第二个中你说 x=y 这意味着 x 的值发生了变化,所以当你再次调用它时,它会是相同的值,因此为什么它是 true 并且 x ==x

            【讨论】:

              【解决方案11】:

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

              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

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

              【讨论】:

              • 结论是错误的。算术运算符和比较运算符之间的行为没有区别。 x + (x = y)(x = y) + x 将显示与带有比较运算符的原始类似的行为。
              • @JJJ 在 x+(x=y) 和 (x=y)+x 中不涉及比较,只是将 y 值分配给 x 并将其添加到 x。
              • ...是的,这就是重点。 “括号仅在算术表达式中发挥其主要作用,而不在比较表达式中”是错误的,因为算术表达式和比较表达式之间没有区别。
              【解决方案12】:

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

              【讨论】:

              • 答案是错误的。运算符优先级不影响评估顺序。阅读一些投票率最高的答案以获得解释,尤其是this one
              • 正确,实际上我们被教导的方式是限制优先级的错觉出现在所有这些事情中,但正确指出它没有影响,因为评估顺序仍然是从左到右
              【解决方案13】:

              在左侧的第二个比较中很容易将 y 分配给 x(在左侧)之后进行分配,然后比较 3 == 3。在第一个示例中,您将 x = 1 与新的分配 x = 3 进行比较。似乎总是从 x 的左到右采取当前状态阅读语句。

              【讨论】:

                【解决方案14】:

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

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

                【讨论】:

                • “要理解代码的作用,必须确切地知道 Java 语言是如何定义的。”但是,如果每个同事都认为这是常识呢?
                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2021-06-07
                • 1970-01-01
                • 2020-06-26
                • 1970-01-01
                • 2015-07-02
                • 1970-01-01
                相关资源
                最近更新 更多