Java中的评估顺序规则是什么?

2022-08-31 11:27:34

我正在阅读一些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 = 0a[b] = (b = 0);b=0

(更新:这个答案的早期版本在后面的部分中有一些小的,实际上不重要的遗漏,我已经纠正了。我还写了一篇博客文章,描述了为什么这些规则在Java和C#中是明智的:https://ericlippert.com/2019/01/18/indexer-error-cases/)

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

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

规则是,再次,“当你可以选择先做什么时,总是从左到右”。但是,在这种特定情况下有一个有趣的皱纹。由 null 集合或超出范围的索引引起的引发异常的副作用是被视为赋值左侧计算的一部分,还是被视为赋值本身计算的一部分?Java选择了后者。(当然,这种区别只有在代码已经错误时才重要,因为正确的代码首先不会取消引用null或传递错误的索引。

那么会发生什么呢?

  • 的左侧是 ,因此首先运行,从而产生 .但是,检查此索引操作的有效性会延迟。a[b]b=0a[b]a[1]
  • 然后发生了。b=0
  • 然后进行有效且在范围内的验证aa[1]
  • 值的分配最后发生。a[1]

因此,尽管在这种特定情况下,对于那些不应该首先在正确的代码中发生的罕见错误情况,需要考虑一些微妙之处,但总的来说,您可以推理:左侧的事情先于右侧发生的事情发生。这就是你要找的规则。谈论优先性和关联性既令人困惑又无关紧要。

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

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

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

它们是关于C#的,但大多数这些东西同样适用于Java。


答案 2

埃里克·利珀特(Eric Lippert)精湛的回答仍然没有适当的帮助,因为它谈论的是一种不同的语言。这是Java,其中Java语言规范是语义的明确描述。特别是,§15.26.1是相关的,因为它描述了运算符的评估顺序(我们都知道它是右结合的,是吗?把它减少到我们在这个问题中关心的部分:=

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

  • 首先,计算左侧操作数数组访问表达式的数组引用子表达式。如果此计算突然完成,则赋值表达式由于同样的原因突然完成;不计算索引子表达式(左侧操作数数组访问表达式)和右侧操作数,也不会进行赋值。
  • 否则,将计算左侧操作数数组访问表达式的索引子表达式。如果此计算突然完成,则赋值表达式由于同样的原因突然完成,并且不计算右侧操作数,也不会发生赋值。
  • 否则,将计算右侧操作数。如果此计算突然完成,则赋值表达式由于同样的原因突然完成,并且不会发生赋值。

[...然后,它继续描述赋值本身的实际含义,为了简洁起见,我们可以在这里忽略它......]

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

C和C++在这方面绝对不同于Java:它们的语言定义故意使评估顺序未定义,以实现更多的优化。C#显然像Java,但我对它的文献还不够了解,无法指出正式的定义。(这确实因语言而异,Ruby是严格的L2R,Tcl也是如此 - 尽管由于这里不相关的原因,它本身缺少赋值运算符 - Python是L2R,但在赋值方面是R2L,我觉得这很奇怪,但你去了。


推荐