我在阅读一些Java文本,并获得了以下的代码:
int[] a = {4,4};
int b = 1;
a[b] = b = 0;
在文本中,作者没有给出明确的解释,而最后一行代码的作用是:a[1] = 0;
我不太确定我理解了什么:这个赋值语句是如何被执行的?
我在阅读一些Java文本,并获得了以下的代码:
int[] a = {4,4};
int b = 1;
a[b] = b = 0;
在文本中,作者没有给出明确的解释,而最后一行代码的作用是:a[1] = 0;
我不太确定我理解了什么:这个赋值语句是如何被执行的?
让我非常清楚地说,因为人们经常误解这一点:
子表达式的求值顺序与结合性和优先级都是独立的。 结合性和优先级确定了操作符执行的顺序,但不确定 子表达式的求值顺序。您的问题是关于子表达式求值的顺序。
考虑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# 中这些规则是合理的:https://ericlippert.com/2019/01/18/indexer-error-cases/)
优先级和结合性只告诉我们在 b
被赋值之前,零被赋值给 b
,因为零的赋值计算出在索引操作中被赋值的值。优先级和结合性本身并不说明 a[b]
是在 b=0
之前还是之后被计算。
同样的道理,A()[B()] = C()
和 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。
=
运算符的求值顺序(我们都知道它是右结合的,对吗?)。在此问题中关心的部分缩小到:
如果左操作数表达式是数组访问表达式(§15.13),则需要执行许多步骤:
- 首先,将评估左操作数数组访问表达式的数组引用子表达式。如果此评估突然完成,则由于相同原因,赋值表达式也会突然完成;不会评估左操作数数组访问表达式的索引子表达式和右操作数,也不会进行赋值。
- 否则,将评估左操作数数组访问表达式的索引子表达式。如果此评估突然完成,则由于相同原因,赋值表达式也会突然完成,并且不会评估右操作数,也不会进行赋值。
- 否则,将评估右操作数。如果此评估突然完成,则由于相同原因,将突然完成赋值表达式,并且不会进行赋值。
[...然后继续描述分配本身的实际含义,为了简洁起见,我们可以忽略它...]
简而言之,Java有一个非常严格定义的求值顺序,在任何运算符或方法调用的参数中基本上都是从左到右。数组赋值是比较复杂的情况之一,但即使在那里也仍然是从左到右。 (JLS建议您不要编写需要这些复杂语义约束的代码,我也是这样认为:每个语句只有一个赋值就足够让您陷入麻烦!)a[-1]=c
中,会先计算 c
,然后才会认为 -1
是无效的。 - ZhongYua[b] = b = 0;
1) 数组索引运算符比赋值运算符的优先级更高(参见this answer):
(a[b]) = b = 0;
2) 根据JLS的15.26章节,共有12个赋值运算符;所有这些运算符在语法上都是从右往左结合的。因此,a=b=c表示为a=(b=c),它将c的值赋给b,然后将b的值赋给a。
(a[b]) = (b=0);
3)根据JLS第15.7节的规定:
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);
100/10*2
100/10*2
=100/10
=10*2
=20
第三步:方程式现在处于以下执行状态:
=3+20-13
=3+20
=23
=23-13
=10
编译后的正确输出为10。
再次强调,在解决这些问题时,拥有一张运算符优先级规则和结合性表格非常重要,例如http://introcs.cs.princeton.edu/java/11precedence/。
10-4-3
。 - Pshemo