Java中求值顺序的规则是什么?
我正在阅读一些Java文本,得到以下代码:
int[] a = {4,4};
int b = 1;
a[b] = b = 0;
在文本中,作者没有给出明确的解释,最后一行的效果是: a[1] = 0;
我不是所以我确信我明白:评估是如何发生的?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(5)
让我说得很清楚,因为人们一直误解这一点:
子表达式的求值顺序独立于结合性和优先级。关联性和优先级确定运算符的执行顺序,但不确定子表达式的求值顺序。您的问题是关于子表达式的计算顺序。
考虑
A() + B() + C() * D()
。乘法的优先级高于加法,而加法是左关联的,因此这相当于(A() + B()) + (C() * D())
但知道这只能说明你知道第一次加法将在第二次加法之前发生,并且乘法将在第二次加法之前发生。 它没有告诉你 A()、B()、C() 和 D() 将以什么顺序被调用!(它也没有告诉你乘法是发生在第一个乘法之前还是之后)加法。)完全有可能通过将其编译为以下方式来遵守优先级和结合性规则:所有优先级和结合性规则都遵循 - 第一个加法发生在第二个加法之前,并且乘法发生在第二次加法之前。显然,我们可以以任意顺序调用 A()、B()、C() 和 D(),并且仍然遵守优先级和关联性规则!
我们需要一个与优先级和结合性规则无关的规则来解释子表达式的求值顺序。 Java(和 C#)中的相关规则是“子表达式从左到右求值”。由于 A() 出现在 C() 的左侧,因此首先对 A() 求值,事实上,C() 涉及乘法,而 A() 只涉及加法。
所以现在您有足够的信息来回答您的问题。在
a[b] = b = 0
中,结合性规则表示这是a[b] = (b = 0);
但这并不意味着code>b=0
首先运行!优先级规则规定索引的优先级高于赋值,但这并不意味着索引器在最右边的赋值之前运行。(更新:这个答案的早期版本在我已更正的部分中存在一些小的且实际上不重要的遗漏。我还写了一篇博客文章,描述为什么这些规则在 Java 和 C# 中是明智的:https://ericlippert.com/2019/01/18/indexer-error-cases/)
优先级和关联性仅告诉我们将零分配给
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。
Let me say this very clearly, because people misunderstand this all the time:
Order of evaluation of subexpressions is independent of both associativity and precedence. Associativity and precedence determine in what order the operators are executed but do not determine in what order the subexpressions are evaluated. Your question is about the order in which subexpressions are evaluated.
Consider
A() + B() + C() * D()
. Multiplication is higher precedence than addition, and addition is left-associative, so this is equivalent to(A() + B()) + (C() * D())
But knowing that only tells you that the first addition will happen before the second addition, and that the multiplication will happen before the second addition. It does not tell you in what order A(), B(), C() and D() will be called! (It also does not tell you whether the multiplication happens before or after the first addition.) It would be perfectly possible to obey the rules of precedence and associativity by compiling this as:All the rules of precedence and associativity are followed there -- the first addition happens before the second addition, and the multiplication happens before the second addition. Clearly we can do the calls to A(), B(), C() and D() in any order and still obey the rules of precedence and associativity!
We need a rule unrelated to the rules of precedence and associativity to explain the order in which the subexpressions are evaluated. The relevant rule in Java (and C#) is "subexpressions are evaluated left to right". Since A() appears to the left of C(), A() is evaluated first, regardless of the fact that C() is involved in a multiplication and A() is involved only in an addition.
So now you have enough information to answer your question. In
a[b] = b = 0
the rules of associativity say that this isa[b] = (b = 0);
but that does not mean that theb=0
runs first! The rules of precedence say that indexing is higher precedence than assignment, but that does not mean that the indexer runs before the rightmost assignment.(UPDATE: An earlier version of this answer had some small and practically unimportant omissions in the section which follows which I have corrected. I've also written a blog article describing why these rules are sensible in Java and C# here: https://ericlippert.com/2019/01/18/indexer-error-cases/)
Precedence and associativity only tell us that the assignment of zero to
b
must happen before the assignment toa[b]
, because the assignment of zero computes the value that is assigned in the indexing operation. Precedence and associativity alone say nothing about whether thea[b]
is evaluated before or after theb=0
.Again, this is just the same as:
A()[B()] = C()
-- All we know is that the indexing has to happen before the assignment. We don't know whether A(), B(), or C() runs first based on precedence and associativity. We need another rule to tell us that.The rule is, again, "when you have a choice about what to do first, always go left to right". However, there is an interesting wrinkle in this specific scenario. Is the side effect of a thrown exception caused by a null collection or out-of-range index considered part of the computation of the left side of the assignment, or part of the computation of the assignment itself? Java chooses the latter. (Of course, this is a distinction that only matters if the code is already wrong, because correct code does not dereference null or pass a bad index in the first place.)
So what happens?
a[b]
is to the left of theb=0
, so thea[b]
runs first, resulting ina[1]
. However, checking the validity of this indexing operation is delayed.b=0
happens.a
is valid anda[1]
is in range happensa[1]
happens last.So, though in this specific case there are some subtleties to consider for those rare error cases that should not be occurring in correct code in the first place, in general you can reason: things to the left happen before things to the right. That's the rule you're looking for. Talk of precedence and associativity is both confusing and irrelevant.
People get this stuff wrong all the time, even people who should know better. I have edited far too many programming books that stated the rules incorrectly, so it is no surprise that lots of people have completely incorrect beliefs about the relationship between precedence/associativity, and evaluation order -- namely, that in reality there is no such relationship; they are independent.
If this topic interests you, see my articles on the subject for further reading:
http://blogs.msdn.com/b/ericlippert/archive/tags/precedence/
They are about C#, but most of this stuff applies equally well to Java.
埃里克·利珀特(Eric Lippert)的精彩回答仍然没有多大帮助,因为它谈论的是另一种语言。这就是Java,其中Java语言规范是语义的明确描述。特别是 §15.26.1是相关的,因为它描述了
=
运算符的求值顺序(我们都知道它是右关联的,是吗?)。将其简化为我们在这个问题中关心的部分:[…然后它继续描述赋值本身的实际含义,为简洁起见,我们可以在这里忽略它…]
简而言之,Java 有一个非常严格定义的 评估顺序 在参数中几乎完全是从左到右任何运营商或方法调用。数组赋值是更复杂的情况之一,但即使如此,它仍然是 L2R。 (JLS 确实建议您不要编写需要此类复杂语义约束的代码,我也是如此:每个语句仅进行一次赋值就可能陷入足够多的麻烦!)
C C++ 和 C++ 在这方面与 Java 绝对不同:它们的语言定义故意未定义求值顺序,以实现更多优化。 C# 显然很像 Java,但我不太了解它的文献,无法指出正式的定义。 (不过,这确实因语言而异,Ruby 是严格的 L2R,Tcl 也是如此——尽管由于此处不相关的原因,它本身缺少赋值运算符——而 Python 是 L2R 但在赋值方面 R2L,我觉得很奇怪,但就是这样。)
Eric Lippert's masterful answer is nonetheless not properly helpful because it is talking about a different language. This is Java, where the Java Language Specification is the definitive description of the semantics. In particular, §15.26.1 is relevant because that describes the evaluation order for the
=
operator (we all know that it is right-associative, yes?). Cutting it down a little to the bits that we care about in this question:[… it then goes on to describe the actual meaning of the assignment itself, which we can ignore here for brevity …]
In short, Java has a very closely defined evaluation order that is pretty much exactly left-to-right within the arguments to any operator or method call. Array assignments are one of the more complex cases, but even there it's still L2R. (The JLS does recommend that you don't write code that needs these sorts of complex semantic constraints, and so do I: you can get into more than enough trouble with just one assignment per statement!)
C and C++ are definitely different to Java in this area: their language definitions leave evaluation order undefined deliberately to enable more optimizations. C# is like Java apparently, but I don't know its literature well enough to be able to point to the formal definition. (This really varies by language though, Ruby is strictly L2R, as is Tcl — though that lacks an assignment operator per se for reasons not relevant here — and Python is L2R but R2L in respect of assignment, which I find odd but there you go.)
1)数组索引运算符的优先级高于赋值运算符(请参阅此答案):
2)根据到 15.26。 JLS 的赋值运算符
3) 根据15.7。 JLS 的评估顺序
和
因此:
a)
(a[b])
首先评估为a[1]
b) 然后
(b=0)
评估为0
c)
(a[1] = 0)
最后计算1) array indexing operator has higher precedence then assignment operator (see this answer):
2) According to 15.26. Assignment Operators of JLS
3) According to 15.7. Evaluation Order of JLS
and
So:
a)
(a[b])
evaluated first toa[1]
b) then
(b=0)
evaluated to0
c)
(a[1] = 0)
evaluated last您的代码相当于:
它解释了结果。
Your code is equivalent to:
which explains the result.
考虑下面另一个更深入的例子。
一般经验法则:
在解决这些问题时,最好有一个优先顺序规则和关联性的表格可供阅读,例如 http://introcs.cs.princeton.edu/java/11precedence/
这是一个很好的例子:
问题:什么是上面一行的输出?
答案:应用优先级和结合性规则
步骤 1:根据优先级规则:/ 和 * 运算符优先于 + - 运算符。因此,执行该等式的起点将缩小为:
步骤 2:根据规则和优先级:/ 和 * 的优先级相等。
由于 / 和 * 运算符的优先级相同,因此我们需要查看这些运算符之间的关联性。
根据这两个特定运算符的关联规则,
我们开始从左到右执行方程,即首先执行 100/10:
步骤 3:方程现在处于以下执行状态:
根据规则和优先级:+ 和 - 的优先级相等。
我们现在需要查看运算符 + 和 - 运算符之间的结合性。根据这两个特定运算符的结合性,
我们开始从左到右执行方程,即首先执行 3+20:
10 是编译时的正确输出
同样,在解决这些问题时,有一个优先顺序规则和关联性表非常重要,例如 < a href="http://introcs.cs.princeton.edu/java/11precedence/" rel="nofollow noreferrer">http://introcs.cs.princeton.edu/java/11precedence/
Consider another more in-depth example below.
As a General Rule of Thumb:
It's best to have a table of the Order of Precedence Rules and Associativity available to read when solving these questions e.g. http://introcs.cs.princeton.edu/java/11precedence/
Here is a good example:
Question: What's the Output of the above Line?
Answer: Apply the Rules of Precedence and Associativity
Step 1: According to rules of precedence: / and * operators take priority over + - operators. Therefore the starting point to execute this equation will the narrowed to:
Step 2: According to the rules and precedence: / and * are equal in precedence.
As / and * operators are equal in precedence, we need to look at the associativity between those operators.
According to the ASSOCIATIVITY RULES of these two particular operators,
we start executing the equation from the LEFT TO RIGHT i.e. 100/10 gets executed first:
Step 3: The equation is now in the following state of execution:
According to the rules and precedence: + and - are equal in precedence.
We now need to look at the associativity between the operators + and - operators. According to the associativity of these two particular operators,
we start executing the equation from the LEFT to RIGHT i.e. 3+20 gets executed first:
10 is the correct output when compiled
Again, it is important to have a table of the Order of Precedence Rules and Associativity with you when solving these questions e.g. http://introcs.cs.princeton.edu/java/11precedence/