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


86

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

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

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

我不确定自己是否理解:评估是如何发生的?


19
下面关于发生这种情况的原因的混乱意味着,您永远不要这样做,因为许多人将不得不思考它的实际作用,而不是显而易见。
Martijn

对于这样的问题,正确的答案是“不要那样做!” 作业应视为陈述;使用赋值作为返回值的表达式应该引起编译器错误,或者至少是警告。不要那样做
梅森·惠勒

Answers:


173

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

子表达式的评估顺序与关联性和优先级无关。结合性和优先级确定以什么顺序运营商执行,但以什么顺序确定的子表达式进行评估。您的问题与子表达式的计算顺序有关。

考虑一下A() + B() + C() * D()。乘法比加法具有更高的优先级,并且加法是左关联的,所以这等效于(A() + B()) + (C() * D()) But。但是,知道这仅告诉您第一次加法将在第二次加法之前发生,而乘法将在第二次加法之前发生。它不会告诉您将以什么顺序调用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()的左侧,因此无论C()参与乘法而A()仅涉及加法首先评估A()。

因此,现在您有足够的信息来回答您的问题。在a[b] = b = 0关联性规则中说这是a[b] = (b = 0);事实,但这并不意味着b=0先行!优先级规则说索引比分配优先级更高,但这并不意味着索引器在最右边的分配之前运行

(更新:此答案的较早版本在随后的部分中进行了一些小的且实际上不重要的遗漏,我已对其进行了更正。我还在此处撰写了一篇博客文章,描述了为什么这些规则在Java和C#中是明智的:https:// ericlippert.com/2019/01/18/indexer-error-cases/

优先级和关联性仅告诉我们,to的赋值b必须to的赋值之前发生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。


6
我个人更喜欢心理模型,在第一步中,您要使用优先级和关联性来构建表达式树。在第二步中,从根开始递归评估该树。对节点的评估为:从左至右评估直接子节点,然后评估注释本身。| 该模型的一个优点是它可以轻松地处理二进制运算符具有副作用的情况。但是主要优点是它更适合我的大脑。
CodesInChaos 2011年

我对C ++不能保证这一点吗?那Python呢?
Neil G

2
@Neil:C ++不保证任何有关评估顺序的事情,也从未做过任何保证。(C也没有。)Python严格按优先级顺序进行保证。与其他所有东西不同,分配是R2L。
Donal Fellows

2
@aroth对我来说你听起来很困惑。优先权规则仅意味着必须先对孩子进行评估,然后再对父母进行评估。但是他们对孩子的评估顺序一无所知。Java和C#从左到右选择,C和C ++选择未定义的行为。
CodesInChaos

6
@noober:好的,请考虑:M(A()+ B(),C()* D(),E()+ F())。您希望以什么顺序评估子表达式?是否应该在A(),B(),E()和F()之前评估C()和D(),因为乘法的优先级高于加法?可以很容易地说“显然”顺序应该不同。提出涵盖所有情况的实际规则要困难得多。C#和Java的设计者选择了一个简单的,易于解释的规则:“从左到右”。您提议的替代方案是什么?为什么您认为自己的规则更好?
埃里克·利珀特

32

然而,埃里克·利珀特(Eric Lippert)的出色回答并没有帮助您,因为它在谈论另一种语言。这是Java,其中Java语言规范是对语义的明确描述。尤其是§15.26.1是相关的,因为它描述了=操作员的评估顺序(我们都知道它是右关联的,是吗?)。将其减少到我们在此问题中关心的位:

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

  • 首先,评估左侧操作数数组访问表达式的数组引用子表达式。如果该评估突然完成,则赋值表达式由于相同的原因而突然完成;(左操作数数组访问表达式的)索引子表达式和右操作数不求值,并且不发生赋值。
  • 否则,将评估左侧操作数数组访问表达式的索引子表达式。如果该评估突然完成,则由于相同的原因,赋值表达式会突然完成,并且不会评估右侧操作数并且不会发生赋值。
  • 否则,将评估右侧操作数。如果该评估突然完成,则赋值表达式由于相同的原因而突然完成,并且不会发生赋值。

[…然后继续描述作业本身的实际含义,为简洁起见,在此可以忽略……]

简而言之,Java具有非常严格定义的评估顺序,该顺序几乎在任何运算符或方法调用的参数内从左到右。数组分配是较为复杂的情况之一,但即使在这里,它仍然是L2R。(JLS确实建议您不要编写需要这种复杂语义约束的代码,我也一样:您只需为每个语句分配一个指令就可以解决很多麻烦!)

C和C ++在这方面肯定与Java不同:它们的语言定义故意使评估顺序未定义,以实现更多优化。C#显然类似于Java,但是我对它的文献了解不足,无法指向正式的定义。(尽管这实际上因语言而异,但Ruby严格来说是T2R,而Tcl严格来说是L2R,尽管它本身缺少赋值运算符(出于与此处无关的原因),Python在赋值方面L2R却是R2L,我觉得很奇怪,但是您去了)


11
那么,您说的是Eric的答案是错误的,因为Java专门将其定义为与他所说的完全一样?
Configurator

8
Java(和C#)中的相关规则是“子表达式从左到右求值” -听起来像他在谈论这两个。
配置器

2
这里有些困惑-这是否会使埃里克·利珀特(Eric Lippert)的上述答案变得不那么正确,还是仅仅引用了关于其正确性的具体参考?
GreenieMeanie 2011年

5
@Greenie:Eric的回答是正确的,但是正如我所说,您不能不小心地从该领域的一种语言中获得见识并将其应用于另一种语言。因此,我引用了权威消息来源。
Donal Fellows,

1
奇怪的是,在解析左手变量之前先评估右手;中的a[-1]=cc被评估-1为无效之前。
2015年

5
a[b] = b = 0;

1)数组索引运算符的优先级高于赋值运算符的优先级(请参阅此答案):

(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)最后评估


1

您的代码等效于:

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

这解释了结果。


7
问题是为什么会这样。

@Mat答案是因为考虑到问题中提供的代码,这是在后台进行的操作。这就是评估的过程。
杰罗姆Verstrynge

1
是的我知道。通过IMO仍然没有回答这个问题,这就是为什么这是评估的过程。

1
@Mat'为什么会这样进行评估?' 不是问的问题。“评估是如何发生的?” 是问的问题。
杰罗姆Verstrynge

1
@JVerstry:它们如何不相等?左侧操作数的数组引用子表达式最左侧的操作数。因此,说“首先做最左边的”与说“首先做数组引用”完全相同。如果Java规范作者选择在解释此特定规则时过于冗长和冗长,这对他们有好处。这种事情很令人困惑,应该多而不是少罗word。但是我看不到我的简洁表征在语义上与prolix表征有何不同。
埃里克·利珀特

0

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

作为一般经验法则:

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

这是一个很好的例子:

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

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

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

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

100/10*2

步骤2:根据规则和优先级:/和*优先级相等。

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

根据这两个特定运算符的关联规则,我们从左至右开始执行方程,即首先执行100/10:

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

步骤3:该方程式现在处于以下执行状态:

=3+20-13

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

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

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

编译时10是正确的输出

同样,解决这些问题时,与您一起拥有顺序顺序规则和关联性表非常重要,例如http://introcs.cs.princeton.edu/java/11precedence/


1
您是说“运算符+和-运算符之间的关联性”是“从左到右”。尝试使用该逻辑并进行评估10 - 4 - 3
Pshemo '16

1
我怀疑这个错误可能是由于以下事实引起的:introcs.cs.princeton.edu/java/11precedence的 顶部+是一元运算符(具有从右到左的关联性),但是加号+和-就像乘法* /%左正确的关联性。
Pshemo '16

很好的发现并据此进行了修正,谢谢Pshemo
Mark Burleigh

这个答案解释了优先级和关联性,但是,正如Eric Lippert解释的那样,问题在于评估顺序,这是非常不同的。事实上,这并不能回答问题。
Fabio说恢复Monica
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.