Java中评估顺序的规则是啥?

Posted

技术标签:

【中文标题】Java中评估顺序的规则是啥?【英文标题】:What are the rules for evaluation order in Java?Java中评估顺序的规则是什么? 【发布时间】:2011-10-11 15:36:57 【问题描述】:

我正在阅读一些 Java 文本并得到以下代码:

int[] a = 4,4;
int b = 1;
a[b] = b = 0;

文中作者没有给出明确的解释,最后一行的效果是:a[1] = 0;

我不太确定我是否理解:评估是如何进行的?

【问题讨论】:

顺便说一句,下面关于为什么会发生这种情况的困惑意味着,你永远不应该这样做,因为许多人将不得不考虑它实际上做了什么,而不是显而易见。 对此类问题的正确答案是“不要那样做!”分配应被视为陈述;将赋值用作返回值的表达式应该会引发编译器错误,或者至少会引发警告。不要那样做。 【参考方案1】:

让我说得很清楚,因为人们一直误解这一点:

子表达式的求值顺序独立独立于关联性和优先级。关联性和优先级决定 运算符 的执行顺序,但 决定 子表达式 的评估顺序。您的问题是关于评估 子表达式 的顺序。

考虑A() + B() + C() * D()。乘法的优先级高于加法,加法是左结合的,所以这相当于(A() + B()) + (C() * D()) 但知道这只会告诉你第一次加法将在第二次加法之前发生,并且乘法将在第二次加法之前发生。 它不会告诉你 A()、B()、C() 和 D() 的调用顺序!(它也不会告诉你乘法是发生在第一个之前还是之后另外。)通过将其编译为:

完全有可能遵守优先级和关联性的规则:
d = D()          // these four computations can happen in any order
b = B()
c = C()
a = A()
sum = a + b      // these two computations can happen in any order
product = c * d
result = sum + product // this has to happen last

这里遵循所有优先级和关联性规则——第一次加法发生在第二次加法之前,乘法发生在第二次加法之前。显然,我们可以按任何顺序调用 A()、B()、C() 和 D(),并且仍然遵守优先级和关联性规则!

我们需要一个与优先级和关联性规则无关的规则来解释子表达式的计算顺序。 Java(和 C#)中的相关规则是“从左到右评估子表达式”。 由于 A() 出现在 C() 的左侧,因此首先评估 A(),不管事实上,C() 涉及乘法,而 A() 仅涉及加法。

所以现在您有足够的信息来回答您的问题。在a[b] = b = 0 中,关联规则说这是a[b] = (b = 0);,但这并不意味着b=0 先运行!优先级规则说索引的优先级高于赋值,但这并不意味着索引器在最右边的赋值之前运行

(更新:此答案的早期版本在我已更正的后面部分中有一些小的且实际上不重要的遗漏。我还写了一篇博客文章,描述为什么这些规则在 Java 和 C# 中是明智的:@ 987654321@)

优先级和关联性仅告诉我们零分配b必须发生在之前分配到a[b],因为零分配计算值在索引操作中分配的。单独的优先级和关联性并不能说明a[b] 是在之前 还是 b=0 之后进行评估。

同样,这与:A()[B()] = C() 相同——我们所知道的是索引必须在分配之前发生。我们不知道 A()、B() 还是 C() 是否首先运行基于优先级和关联性。我们需要另一个规则来告诉我们。

同样,规则是“当你可以选择先做什么时,总是从左到右”。但是,在这种特定情况下存在一个有趣的问题。 空集合或超出范围的索引导致的抛出异常的副作用是否被认为是赋值左侧计算的一部分,还是赋值本身计算的一部分? Java 选择了后者。 (当然,这个区别只在代码已经错误的情况下才有意义,因为正确的代码首先不会取消引用 null 或传递错误的索引。)

那么会发生什么?

a[b] 位于b=0 的左侧,因此a[b] 运行首先,导致a[1]。但是,检查此索引操作的有效性会有所延迟。 然后b=0 发生。 然后验证a 有效且a[1] 在范围内 将值分配给a[1] 最后发生。

因此,尽管在这个特定案例中,对于那些原本不应该出现在正确代码中的罕见错误案例,需要考虑一些微妙之处,但通常你可以推理:左边的事情发生在右边的事情之前。这就是您要寻找的规则。谈论优先级和关联性既令人困惑又无关紧要。

人们总是弄错这些东西,即使是那些应该知道得更多的人。我编辑了 太多 错误地说明规则的编程书籍,所以很多人对优先级/关联性和评估顺序之间的关系有完全错误的信念也就不足为奇了——也就是说,实际上没有这种关系;他们是独立的。

如果您对此主题感兴趣,请参阅我关于该主题的文章以进一步阅读:

http://blogs.msdn.com/b/ericlippert/archive/tags/precedence/

它们是关于 C# 的,但其中大部分内容同样适用于 Java。

【讨论】:

就我个人而言,我更喜欢在第一步中使用优先级和关联性构建表达式树的心智模型。在第二步中,从根开始递归地评估该树。对节点的评估是:从左到右评估 immediate 子节点,然后评估音符本身。 |该模型的一个优点是它可以轻松处理二元运算符具有副作用的情况。但主要优点是它更适合我的大脑。 C++ 不能保证这一点我是否正确? Python 呢? @Neil:C++ 不保证任何关于评估顺序的事情,而且从来没有。 (C 也没有。)Python 严格按照优先顺序保证它;与其他一切不同,赋值是 R2L。 @aroth 对我来说你听起来很困惑。而优先规则仅意味着孩子需要在父母之前进行评估。但是他们对孩子的评估顺序只字未提。 Java 和 C# 选择从左到右,C 和 C++ 选择未定义行为。 @noober:好的,考虑:M(A() + B(), C() * D(), E() + F())。您希望以什么顺序评估子表达式?是否应该在 A()、B()、E() 和 F() 之前评估 C() 和 D(),因为乘法的优先级高于加法?很容易说“显然”顺序应该不同。想出一个涵盖所有情况的实际规则相当困难。 C# 和 Java 的设计者选择了一个简单、易于解释的规则:“从左到右”。你提议的替代方案是什么,为什么你认为你的规则更好?【参考方案2】:

尽管如此,Eric Lippert 的巧妙回答并没有提供适当的帮助,因为它谈论的是另一种语言。这就是 Java,其中 Java 语言规范是语义的最终描述。特别是,§15.26.1 是相关的,因为它描述了 = 运算符的评估顺序(我们都知道它是右关联的,是吗?)。将其减少到我们在这个问题中关心的部分:

如果左侧操作数表达式是数组访问表达式 (§15.13),则需要执行很多步骤:

首先,计算左侧操作数数组访问表达式的数组引用子表达式。如果这个求值突然完成,那么赋值表达式也会因为同样的原因而突然完成; (左侧操作数数组访问表达式的)索引子表达式和右侧操作数不会被计算,也不会发生赋值。 否则,将计算左侧操作数数组访问表达式的索引子表达式。如果此求值突然完成,则赋值表达式出于同样的原因突然完成,右侧操作数不会被求值,也不会发生赋值。 否则,将计算右侧操作数。如果此求值突然完成,则赋值表达式出于同样的原因突然完成并且不会发生赋值。

[……它接着描述了赋值本身的实际含义,为简洁起见,我们可以在这里忽略……]

简而言之,Java 有一个非常紧密定义的evaluation order,它在任何运算符或方法调用的参数中几乎完全是从左到右的。数组分配是更复杂的情况之一,但即便如此,它仍然是 L2R。 (JLS 确实建议您不要编写需要这些复杂语义约束的代码,我也是如此:每个语句只需一个赋值就可能会遇到很多麻烦!)

C 和 C++ 在这方面与 Java 绝对不同:它们的语言定义故意未定义评估顺序以实现更多优化。 C# 显然就像 Java,但我对它的文献了解得不够充分,无法指出正式的定义。 (这确实因语言而异,Ruby 严格来说是 L2R,Tcl 也是如此——尽管它缺少赋值运算符 per se,原因与这里无关——而 Python 是 L2R but R2L in respect of assignment,我觉得这很奇怪,但给你。)

【讨论】:

那么你的意思是 Eric 的回答是错误的,因为 Java 专门将它定义为正是他所说的? Java(和 C#)中的相关规则是“从左到右评估子表达式” - 我听上去好像他在谈论两者。 这里有点困惑 - 这是否使 Eric Lippert 的上述回答变得不那么真实,或者它只是引用了一个具体的参考来说明为什么它是正确的? @Greenie:Eric 的回答是正确的,但正如我所说,您不能从该领域的一种语言中获得洞察力并将其应用于另一种语言而不小心。所以我引用了明确的来源。 奇怪的是,在解析左手变量之前先计算右手;在a[-1]=c 中,c 被评估,在-1 被识别为无效之前。【参考方案3】:
a[b] = b = 0;

1) 数组索引运算符的优先级高于赋值运算符(参见this answer):

(a[b]) = b = 0;

2) 根据 15.26。 JLS的赋值运算符

有12个赋值运算符;所有在语法上都是右关联的(它们从右到左分组)。因此,a=b=c 表示 a=(b=c),将 c 的值赋给 b,然后将 b 的值赋给 a。

(a[b]) = (b=0);

3) 根据 15.7。 JLS的评估顺序

Java 编程语言保证运算符的操作数看起来是按照特定的计算顺序计算的,即从左到右。

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

所以:

a) (a[b]) 首先评估为 a[1]

b) 然后(b=0) 评估为0

c) (a[1] = 0) 最后评估

【讨论】:

【参考方案4】:

你的代码相当于:

int[] a = 4,4;
int b = 1;
c = b;
b = 0;
a[c] = b;

这解释了结果。

【讨论】:

问题是为什么会这样。 @Mat 答案是因为考虑到问题中提供的代码,这就是幕后发生的事情。这就是评估的发生方式。 是的,我知道。尽管 IMO 仍然没有回答这个问题,这就是 为什么这是评估发生的方式。 @Mat '为什么评估会这样?不是问的问题。 “评估是怎么发生的?”是被问到的问题。 @JVerstry:它们怎么不相等?左侧操作数的数组引用子表达式是最左侧的操作数。所以说“先做最左边”与说“先做数组引用”完全一样。如果 Java 规范作者在解释这个特定规则时选择了不必要的冗长和冗余,那对他们有好处;这种事情令人困惑,应该更多而不是更少罗嗦。但我看不出我的简洁表征在语义上与他们的冗长表征有何不同。【参考方案5】:

请考虑下面另一个更深入的示例。

作为一般经验法则:

在解决这些问题时,最好有一个优先顺序规则和关联性的表格可供阅读,例如http://introcs.cs.princeton.edu/java/11precedence/

这是一个很好的例子:

System.out.println(3+100/10*2-13);

问题:上面一行的输出是什么?

答案:应用优先规则和关联性规则

第1步:根据优先规则:/和*运算符优先于+-运算符。因此,执行这个方程的起点将缩小到:

100/10*2

第二步:根据规则和优先级:/和*优先级相等。

由于 / 和 * 运算符的优先级相等,我们需要查看这些运算符之间的关联性。

根据这两个特定运营商的关联规则, 我们从左到右开始执行等式,即首先执行 100/10:

100/10*2
=100/10
=10*2
=20

第 3 步:方程现在处于以下执行状态:

=3+20-13

根据规则和优先级:+和-的优先级相等。

我们现在需要查看运算符 + 和 - 运算符之间的关联性。根据这两个特定运算符的结合性, 我们开始执行从左到右的等式,即首先执行 3+20:

=3+20
=23
=23-13
=10

10 是编译时正确的输出

同样,在解决这些问题时,请务必随身携带一张优先顺序规则和关联性表格,例如: http://introcs.cs.princeton.edu/java/11precedence/

【讨论】:

您是说“运算符 + 和 - 运算符之间的关联性”是“从右到左”。尝试使用该逻辑并评估10 - 4 - 3 我怀疑这个错误可能是由于 introcs.cs.princeton.edu/java/11precedence + 的顶部是一元运算符(具有从右到左的关联性),但加法 + 和 - 就像乘法 * / % 从左到右的关联性。 很好地发现并进行了相应的修改,谢谢 Pshemo 这个答案解释了优先级和关联性,但是,作为Eric Lippert explained,问题是关于评估顺序,这是非常不同的。事实上,这并不能回答问题。

以上是关于Java中评估顺序的规则是啥?的主要内容,如果未能解决你的问题,请参考以下文章

在红移中评估的顺序是啥

MySQL JOIN 的评估顺序是啥?

在这种方法调用和传入参数的情况下,Java 评估顺序是不是得到保证

为啥具有短路操作的并行 Java Stream 会评估 Stream 的所有元素,而顺序 Stream 不会?

短路评估顺序和前缀增量运算符

std::wstring 的标准定义字节顺序是啥?