我正在阅读一些Java文本,并获得以下代码:
int[] a = {4,4};
int b = 1;
a[b] = b = 0;
在本文中,作者没有给出明确的解释,最后一行的效果是: a[1] = 0;
我不确定自己是否理解:评估是如何发生的?
Answers:
让我说得很清楚,因为人们一直误会这一点:
子表达式的评估顺序与关联性和优先级无关。结合性和优先级确定以什么顺序运营商执行,但不以什么顺序确定的子表达式进行评估。您的问题与子表达式的计算顺序有关。
考虑一下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。
然而,埃里克·利珀特(Eric Lippert)的出色回答并没有帮助您,因为它在谈论另一种语言。这是Java,其中Java语言规范是对语义的明确描述。尤其是§15.26.1是相关的,因为它描述了=
操作员的评估顺序(我们都知道它是右关联的,是吗?)。将其减少到我们在此问题中关心的位:
如果左侧操作数表达式是数组访问表达式(第15.13节),则需要许多步骤:
- 首先,评估左侧操作数数组访问表达式的数组引用子表达式。如果该评估突然完成,则赋值表达式由于相同的原因而突然完成;(左操作数数组访问表达式的)索引子表达式和右操作数不求值,并且不发生赋值。
- 否则,将评估左侧操作数数组访问表达式的索引子表达式。如果该评估突然完成,则由于相同的原因,赋值表达式会突然完成,并且不会评估右侧操作数并且不会发生赋值。
- 否则,将评估右侧操作数。如果该评估突然完成,则赋值表达式由于相同的原因而突然完成,并且不会发生赋值。
[…然后继续描述作业本身的实际含义,为简洁起见,在此可以忽略……]
简而言之,Java具有非常严格定义的评估顺序,该顺序几乎在任何运算符或方法调用的参数内从左到右。数组分配是较为复杂的情况之一,但即使在这里,它仍然是L2R。(JLS确实建议您不要编写需要这种复杂语义约束的代码,我也一样:您只需为每个语句分配一个指令就可以解决很多麻烦!)
C和C ++在这方面肯定与Java不同:它们的语言定义故意使评估顺序未定义,以实现更多优化。C#显然类似于Java,但是我对它的文献了解不足,无法指向正式的定义。(尽管这实际上因语言而异,但Ruby严格来说是T2R,而Tcl严格来说是L2R,尽管它本身缺少赋值运算符(出于与此处无关的原因),Python在赋值方面是L2R却是R2L,我觉得很奇怪,但是您去了)
a[-1]=c
,c
被评估-1
为无效之前。
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)
最后评估
您的代码等效于:
int[] a = {4,4};
int b = 1;
c = b;
b = 0;
a[c] = b;
这解释了结果。
请考虑下面的另一个更深入的示例。
解决这些问题时,最好先阅读一下优先顺序规则和关联性表,例如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/
10 - 4 - 3
。
+
是一元运算符(具有从右到左的关联性),但是加号+和-就像乘法* /%左正确的关联性。