Javaでの評価順序のルールは何ですか?


86

私はいくつかのJavaテキストを読んでいて、次のコードを取得しました。

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

本文では、著者は明確な説明をしていませんでした、そして最後の行の効果は次のとおりです: a[1] = 0;

私は理解しているのかよくわかりません:評価はどのように行われたのですか?


19
ちなみに、これが発生する理由に関する以下の混乱は、これを絶対に行わないことを意味します。多くの人は、それが明白であるのではなく、実際に何をするのかを考えなければならないからです。
Martijn 2011

このような質問に対する適切な答えは、「それをしないでください!」です。割り当てはステートメントとして扱う必要があります。値を返す式として割り当てを使用すると、コンパイラエラー、または少なくとも警告が発生するはずです。そうしないでください。
メイソンウィーラー

回答:


173

人々はこれを常に誤解しているので、これを非常に明確に言わせてください。

部分式の評価の順序は、結合性と優先順位の両方に依存しません。結合性と優先順位は、演算子が実行される順序を決定しますが、部分式が評価される順序は決定しません。あなたの質問は、部分式が評価される順序についてです。

考えてみてくださいA() + B() + C() * D()。乗算は加算よりも優先順位が高く、加算は左結合であるため、これは同等です。(A() + B()) + (C() * D()) ただし、最初の加算は2番目の加算の前に行われ、乗算は2番目の加算の前に行われることだけがわかります。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

そこでは、優先順位と結合性のすべての規則に従います。最初の加算は2番目の加算の前に行われ、乗算は2番目の加算の前に行われます。明らかに、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/

優先順位と結合だけであることを教えゼロの割り当てをするb起こる必要がありますに割り当てa[b]ゼロの割り当ては、インデックス操作に割り当てられた値を計算するので、。優先順位と結合規則だけではかどうかについては何も言わないa[b]で評価されるまたは後にb=0

繰り返しますが、これは次の場合とまったく同じA()[B()] = C()です。-割り当ての前にインデックスを作成する必要があることだけがわかっています。優先順位と結合性に基づいて、A()、B()、またはC()が最初実行されるかどうかはわかりません。それを伝えるために別のルールが必要です。

繰り返しになりますが、「最初に何をするかを選択できる場合は、常に左から右に移動する」というルールがあります。ただし、この特定のシナリオには興味深いしわがあります。nullコレクションまたは範囲外のインデックスによって引き起こされたスローされた例外の副作用は、割り当ての左側の計算の一部、または割り当て自体の計算の一部と見なされますか?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
個人的には、最初のステップで優先順位と結合性を使用して式ツリーを構築するメンタルモデルが好きです。そして、2番目のステップでは、ルートから始めてそのツリーを再帰的に評価します。ノードの評価は次のとおりです。直接の子ノードを左から右に評価してから、メモ自体を評価します。| このモデルの利点の1つは、二項演算子に副作用がある場合を簡単に処理できることです。しかし、主な利点は、それが単に私の脳により良くフィットすることです。
CodesInChaos 2011

C ++がこれを保証しないというのは正しいですか?Pythonはどうですか?
ニールG

2
@Neil:C ++は評価の順序について何も保証せず、決して保証しませんでした。(Cもそうではありません。)Pythonは、優先順位によって厳密にそれを保証します。他のすべてとは異なり、割り当てはR2Lです。
ドナーフェロー

2
@aroth私にはあなたはただ混乱しているように聞こえます。そして、優先ルールは、子供が親の前に評価される必要があることを意味するだけです。しかし、彼らは子供たちが評価される順序については何も言いません。JavaとC#は左から右に選択し、CとC ++は未定義の動作を選択しました。
CodesInChaos 2011

6
@noober:OK、検討してください:M(A()+ B()、C()* D()、E()+ F())。部分式がどのような順序で評価されることを望みますか?乗算は加算よりも優先されるため、C()とD()はA()、B()、E()、F()の前に評価する必要がありますか?「明らかに」順序は異なるはずだと言うのは簡単です。すべてのケースをカバーする実際のルールを考え出すのはかなり難しいです。C#とJavaの設計者は、「左から右に移動する」というシンプルで説明しやすいルールを選択しました。それに対するあなたの提案された代替品は何ですか、そしてなぜあなたはあなたのルールがより良いと信じますか?
Eric Lippert 2011

32

それにもかかわらず、エリック・リペットの見事な答えは、別の言語について話しているため、適切に役立ちません。これはJavaであり、Java言語仕様がセマンティクスの決定的な記述です。特に、§15.26.1は、=演算子の評価順序を説明しているため、関連性があります(これは、右結合性であることは誰もが知っていますよね?)。この質問で気になる部分に少し切り詰めます。

左側のオペランド式が配列アクセス式(§15.13)の場合、多くの手順が必要です。

  • 最初に、左側のオペランド配列アクセス式の配列参照部分式が評価されます。この評価が突然完了すると、同じ理由で代入式が突然完了します。(左側のオペランド配列アクセス式の)インデックス部分式と右側のオペランドは評価されず、割り当ては行われません。
  • それ以外の場合は、左側のオペランド配列アクセス式のインデックス部分式が評価されます。この評価が突然完了すると、同じ理由で割り当て式が突然完了し、右側のオペランドは評価されず、割り当ては発生しません。
  • それ以外の場合は、右側のオペランドが評価されます。この評価が突然完了すると、同じ理由で割り当て式が突然完了し、割り当ては発生しません。

[…次に、割り当て自体の実際の意味について説明します。簡潔にするために、ここでは無視できます…]

要するに、Javaには非常に厳密に定義された評価順序があり、演算子またはメソッド呼び出しの引数内でほぼ正確に左から右になります。配列の割り当ては、より複雑なケースの1つですが、それでもL2Rがあります。(JLSは、このような複雑なセマンティック制約を必要とするコードを記述しないことを推奨しています。そのため、ステートメントごとに1つの割り当てだけで十分な問題が発生する可能性があります!)

CとC ++は、この領域でJavaとは明らかに異なります。それらの言語定義では、より多くの最適化を可能にするために、評価順序が意図的に未定義のままになっています。C#は明らかにJavaに似ていますが、正式な定義を示すことができるほどその文献をよく知りません。(これは実際には言語によって異なりますが、Rubyは厳密にL2Rであり、Tclも同様です。ただし、ここでは関係のない理由で代入演算子自体がありません。PythonL2Rですが、代入に関してはR2Lです。これは奇妙だと思いますが、そこに行きます。 。)


11
それで、あなたが言っているのは、Javaがそれを彼が言ったこととまったく同じであると明確に定義しているので、エリックの答えは間違っているということですか?
コンフィギュレー

8
Java(およびC#)の関連ルールは、「部分式は左から右に評価される」です-彼が両方について話しているように私には聞こえます
configurator

2
ここで少し混乱しています-これはEricLippertによる上記の答えを少しでも真実にしませんか、それともなぜそれが真実であるかについての特定の参照を引用しているだけですか?
GreenieMeanie 2011

5
@Greenie:エリックの答えは本当ですが、私が述べたように、この分野のある言語から洞察を得て、注意せずに別の言語に適用することはできません。それで私は決定的な情報源を引用しました。
ドナーフェロー

1
奇妙なことに、左側の変数が解決される前に右側が評価されます。でa[-1]=ccが評価され、前に-1無効として認識されます。
ZhongYu 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仕様の作成者が、この特定のルールを説明する際に不必要に言葉を使い、冗長にすることを選択した場合は、彼らにとって良いことです。この種のことは紛らわしいので、言葉を少なくするのではなく、もっと多くする必要があります。しかし、私の簡潔な特徴付けが、それらのプロリックスの特徴付けとどのように意味的に異なるのかわかりません。
エリック・リペット

0

以下の別のより詳細な例を検討してください。

一般的な経験則として:

http://introcs.cs.princeton.edu/java/11precedence/のように、これらの質問を解決するときに読むことができる優先順位規則と結合性の表を用意しておくことをお勧めします。

これが良い例です:

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

質問:上記の行の出力は何ですか?

回答:優先順位と結合性のルールを適用する

ステップ1:優先順位の規則に従って:/および*演算子は+-演算子よりも優先されます。したがって、この方程式を実行するための開始点は、次のように絞り込まれます。

100/10*2

ステップ2:ルールと優先順位に従って:/と*の優先順位は同じです。

/演算子と*演算子の優先順位は等しいため、これらの演算子間の結合性を調べる必要があります。

これら2つの特定の演算子の結合法則に従って、左から右に方程式の実行を開始します。つまり、100/10が最初に実行されます。

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

ステップ3:方程式は次の実行状態になります。

=3+20-13

ルールと優先順位によると、+と-の優先順位は同じです。

ここで、演算子+演算子と-演算子の間の結合性を調べる必要があります。これら2つの特定の演算子の結合性に従って、左から右に方程式の実行を開始します。つまり、3 +20が最初に実行されます。

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

10はコンパイル時の正しい出力です

繰り返しになりますが、これらの質問を解決するときは、優先順位規則と結合性の表を用意しておくことが重要です。例:http//introcs.cs.princeton.edu/java/11precedence/


1
「演算子+と-演算子間の結合性」は「RIGHTTOLEFT」であるとおっしゃっています。そのロジックを使用して評価してみてください10 - 4 - 3
pshemo 2016年

1
この間違いは、introcs.cs.princeton.edu / java / 11precedenceの上部に+単項演算子(右から左への結合性がある)であるという事実が原因である可能性がありますが、加法+と-は乗法* /%のように残っています右の結合性。
pshemo 2016年

よく発見され、それに応じて修正されました。ありがとうPshemo
Mark Burleigh

この回答は優先順位と結合性を説明していますが、Eric Lippertが説明したように、質問は評価順序に関するものであり、これは非常に異なります。実際のところ、これは質問に答えません。
ファビオは
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.