"&&"和"||"运算符优先级引起困惑的例子

8

我正在测试 &&|| 之间的优先级,我有一个很困惑的例子。在Java中,&& 的运算符优先级高于运算符||

因此,如果我们有以下3个表达式:

//expr1  = true , expr2 = false; expr3 = false;

if(expr1 || expr2 && expr3);

应该进行评估:

if(expr1 || (expr2 && expr3));

所以在执行expr1之前,应该先执行expr2 && expr3。然而,下面这个例子:
int a1 = 10;
int a2 = 20;
System.out.println(a1 < a2 || ++a1 > a2 && ++a2 < a1);
System.out.println(a1);
System.out.println(a2);

输出:

true
10
20

这证明只有 a1 < a2 被评估。 你能解释一下为什么会这样吗?

7
这里的"operators also short-circuit"的意思是运算符也会短路。 - Dave Newton
4
优先级(precedence)并不等同于求值顺序(order of evaluation)。expr2expr3被它所分组,但是它们仍然从左到右被求值。 - Peter Lawrey
2
我认为Eric Lippert关于Precedence vs Associativity vs Order的文章是一个很好的解释。虽然它是关于C#而不是Java,但重要的是要区分评估顺序和运算符优先级,这同样适用于这里。 - Daniel Pryden
我知道这很老,但也许你在考虑 PEMDAS,但在这种情况下,|| 运算符会在括号之前被评估。 - financial_physician
5个回答

15
表达式正在进行“短路计算”。来自链接:https://en.m.wikipedia.org/wiki/Short-circuit_evaluation

当AND函数的第一个参数计算结果为false时,整个表达式的值必须为false;当OR函数的第一个参数计算结果为true时,整个表达式的值必须为true。

它发现条件的其余部分并不重要,因为||的操作数之一已经是true(10 < 20)。如果其中一个操作数为true,则无论条件的其余部分如何,它都是true。

你可以使用按位运算符&和|来防止这种情况。


但是,(expr2 && expr3)不应该在expr1之前计算吗?

不。您必须区分“优先级”和“计算顺序”的概念。

  • 优先级:规定了表达式的括号顺序,而不是表达式求值的顺序。例如:

      true || false && false
    

    因为 && 的优先级高于 ||,所以加括号:

      true || (false && false)
    
    这并不意味着在 Java 的情况下括号中的内容会首先被评估。优先级只是澄清运算符的操作数,本例中为falsefalse,而在这种情况下:

    这并不意味着在 Java 中,括号内的表达式会首先被计算。优先级只是澄清操作符的操作数,即在本例中为falsefalse,而在这种情况下:

      (true || false) && (false || false)
    

    && 运算符的操作数是 truefalse,而不是 falsefalse

  • 求值顺序: 描述每个操作数被求值和运算符被应用的顺序,有时是语言特定的。这决定了表达式的求值顺序,与优先级不同。

在本例中,您的示例:

true || false && false

正如前面所述,这是由于优先级而发生的:

true || (false && false)

但是,与C++、JavaScript或其他许多语言不同,Java有严格的从左到右的求值顺序。根据Java语言规范

15.7.求值顺序

Java编程语言保证运算符的操作数会按照特定的顺序进行求值,即从左到右。

15.7.1.先评估左侧操作数

一个二元操作符的左侧操作数在右侧任何部分被评估之前应该完全被评估。

因此,当你有:

true || (false && false)

Java 首先评估左操作数,结果为true。然后整个条件就短路了。括号中||的右操作数根本不会被评估。对于您的另一个示例也是如此:

a1 < a2 || (++a1 > a2 && ++a2 < a1)
           ^^^^^^^^^^^^^^^^^^^^^^^^
           Step 0, precedence and parenthesization

a1 < a2 || (++a1 > a2 && ++a2 < a1)
^^^^^^^
Step 1, left operand evaluated, variables resolved to values 10 and 20, condition is true

true || (++a1 > a2 && ++a2 < a1)
^^^^
Step 2, short circuits, left operand is not evaluated

拿另一个更复杂的例子:

false || false || true && (false || true) || false

由于先例,它变成:

false || false || (true && (false || true)) || false

然后开始评估,从左到右:

false || false || (true && (false || true)) || false
^^^^^^^^^^^^^^
Step 1, false || false, does not short circuit, right operand is evaluated, is false

false || (true && (false || true)) || false
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Step 2, false || (true && (false || true)), does not short circuit, right operand is evaluated
Step 2A, (true && (false || true)), does not short circuit, right operand is evaluated
Step 2B, (false || true), does not short circuit, right operand is evaluated, is true
Step 2C, (true && true), does not short circuit, right operand is evaluated, is true
Step 2D, false || true, does not short circuit, right operand is evaluated, is true

true || false
^^^^
Step 3, true || false short circuits, right operand is not evaluated, is true

因此,整个表达式评估为true。整个表达式从左到右完全评估。优先级仅通过括号化来指定运算符的操作数,而不是评估顺序。

更多阅读请参见Eric Lippert解释性文章关于优先级与结合性与评估顺序,正如Daniel Pryden所提到的,这消除了很多困惑。

主要的要点是,优先级并不决定表达式的评估方式。它只规定了如何将表达式括起来。另一方面,评估顺序告诉我们表达式的确切评估方式,在Java的情况下始终从左到右。


6
第一行打印出了true,因为短路了||操作符。
由于a1 < a2为true,所以布尔表达式的其余部分无需计算,返回true

应先计算expr2

这是不正确的,因为运算符优先级影响表达式的结构而非计算顺序(在大多数情况下)。如果您重新排列表达式,使其为(expr2 && expr3) || expr1,那么是的,expr2会在expr1之前计算。

5

优先级:

boolean result = a || b && c

为了根据优先级规则得出正确的值,编译器必须逻辑上进行评估:
boolean x = b && c
boolean result = a || x

这与你所说的b && c必须在计算result之前进行评估有关。但是,在布尔代数中,可以编写其结果不依赖于所有操作数的表达式。利用这一事实,可以启用称为“短路”的编译器优化。

短路:

对于任何布尔表达式:

a || b

如果a的结果为true,那么结果不会依赖于b。当atrue时,无论b的结果是true还是false都没有关系。因此,编译器执行:

a || b && c

就好像它已经被写出来一样:

boolean result = a;
if (!a) result = b && c;

注意,在这种方式下,运算优先级仍然会被尊重。表达式不会被评估为(a || b)&& c 。由于表达式的总体结果在a为true的情况下不依赖于(b && c),因此b && c根本不会被评估。
这是一件好事。短路计算允许您编写正确的程序,当一个操作数的正确性取决于另一个操作数的正确性时。例如:
if (myString == null || !myString.isEmpty() && myString != "break") return;

没有短路评估,布尔表达式可能会抛出NullPointerException。 但是,由于短路评估,这个表达式按照现有的形式永远不会抛出NullPointerException

短路也可以用作性能优化。 如果一个操作数的计算非常昂贵,那么首先计算另一个操作数可以节省执行时间,因为该操作数的值不影响整个表达式的最终结果。


3
即使你使用显式的括号,例如
if (expr1 || (expr2 && expr3))
expr1首先被评估,因为操作符的操作数从左到右进行评估。由于|| 是一个短路运算符,如果expr1false,则只有在这种情况下才会评估第二个操作数(在您的情况下为(expr2 && expr3))。
当您删除括号时,只有在expr1为false的情况下,操作符优先级才会起作用。在这种情况下,&&操作符将在||操作符之前进行评估,并且其值将是||操作符的第二个操作数。

0
这是因为短路。首先评估第一个表达式,如果能够得出结果为真,则会在那里结束并且不会继续进行其他表达式的计算。如果第一个表达式的结果为假,则将检查另一个条件并得出结果。
要了解更多运算符优先级,请参阅以下内容。

https://introcs.cs.princeton.edu/java/11precedence/


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