このJavaコードがコンパイルされるのはなぜですか?


96

メソッドまたはクラスのスコープでは、以下の行がコンパイルされます(警告付き):

int x = x = 1;

変数がデフォルト値を取得するクラススコープで、以下は「未定義の参照」エラーを示します。

int x = x + 1;

最初にx = x = 1同じ「未定義の参照」エラーが発生するのではないですか?または、2行目int x = x + 1をコンパイルする必要がありますか?または私が行方不明のものがありますか?


1
のようにキーワードstaticをクラススコープ変数に追加static int x = x + 1;すると、同じエラーが発生しますか?C#では静的か非静的かによって違いが生じるためです。
Jeppe Stig Nielsen

static int x = x + 1Javaでは失敗します。
Marcin 2013

1
おそらく§17.4.5.2-「インスタンスフィールドの変数初期化子は、作成中のインスタンスを参照できません。」が原因で、c#int a = this.a + 1;int b = 1; int a = b + 1;クラススコープ(Javaではどちらでも問題あり)で失敗します。どこかで明示的に許可されているかどうかはわかりませんが、staticにはそのような制限はありません。Javaでは、ルールは異なっており、static int x = x + 1同じ理由で失敗しint x = x + 1ない
MSAM

バイトコードを持つそのanserは疑いをすべてクリアします。
rgripper 2013

回答:


101

tl; dr

以下のためにフィールドint b = b + 1ので違法であるbと違法前方参照ですb。これを実際に修正するには、を記述します。これは、問題int b = this.b + 1なくコンパイルされます。

以下のために、ローカル変数int d = d + 1ので違法であるd、使用する前に初期化されていません。これは、常にデフォルトで初期化されるフィールドには当てはまりません

コンパイルしてみると違いがわかります

int x = (x = 1) + x;

フィールド宣言およびローカル変数宣言として。前者は失敗しますが、セマンティクスの違いにより、後者は成功します。

前書き

まず、フィールドとローカル変数の初期化子のルールは大きく異なります。したがって、この答えは2つの部分でルールに取り組みます。

このテストプログラムは、全体を通して使用します。

public class test {
    int a = a = 1;
    int b = b + 1;
    public static void Main(String[] args) {
        int c = c = 1;
        int d = d + 1;
    }
}

の宣言bは無効であり、illegal forward referenceエラーが発生して失敗します。
の宣言dは無効であり、variable d might not have been initializedエラーが発生して失敗します。

これらのエラーが異なるという事実は、エラーの理由も異なることを示唆しています。

田畑

Javaのフィールド初期化子は、JLS§8.3.2、フィールドの初期化によって管理されます。

スコープフィールドのはで定義されているJLS§6.3、宣言のスコープ。

関連するルールは次のとおりです。

  • mクラス型C(§8.1.6)で宣言または継承されたメンバーの宣言のスコープは、ネストされた型宣言を含むCの本体全体です。
  • インスタンス変数の初期化式では、クラスで宣言または継承された静的変数の単純な名前を使用できます。
  • これらのインスタンス変数がスコープ内であっても、宣言が使用後にテキストで表示されるインスタンス変数の使用が制限される場合があります。インスタンス変数への前方参照を管理する正確な規則については、8.3.2.3を参照してください。

8.3.2.3は言う:

メンバーの宣言は、そのメンバーがクラスまたはインターフェイスCのインスタンス(それぞれ静的)フィールドであり、次のすべての条件が満たされている場合にのみ、使用前にテキストで表示する必要があります。

  • 使用法は、Cのインスタンス(それぞれ静的)変数初期化子またはCのインスタンス(それぞれ静的)初期化子で発生します。
  • 使用法は割り当ての左側ではありません。
  • 使い方は単純な名前です。
  • Cは、使用法を囲む最も内側のクラスまたはインターフェースです。

特定の場合を除いて、フィールドは宣言される前に実際に参照できます。これらの制限は、次のようなコードを防ぐことを目的としています

int j = i;
int i = j;

コンパイルから。Java仕様には、「上記の制限は、コンパイル時に循環または初期化されていない不正な初期化をキャッチするように設計されている」とあります。

これらのルールは実際にはどのように要約されますか?

要するに、ルールは基本的にあなたがいることを言わなければならないの参照がある()参照(b)は、初期化子である参照が割り当てされていない場合(c)は、そのフィールドへの参照の前にフィールドを宣言します単純な名前(のような修飾子なしthis.)および(d)内部クラス内からアクセスされていない。したがって、4つすべての条件を満たす前方参照は不正ですが、少なくとも1つの条件で失敗する前方参照は問題ありません。

int a = a = 1;(b)に違反しているためコンパイルします。参照a 割り当てられているため、の完全な宣言のa前に参照することは合法aです。

int b = this.b + 1(c)に違反しているため、コンパイルも行われます。参照this.bは単純な名前ではありません(これはで修飾されていますthis.)。この奇妙な構成はthis.b、値がゼロであるため、完全に明確に定義されています。

したがって、基本的に、初期化子内のフィールド参照の制限により、int a = a + 1正常にコンパイルできません。

final は依然として不正な前方参照であるため、フィールド宣言int b = (b = 1) + bがコンパイルに失敗することに注意bしてください。

ローカル変数

ローカル変数宣言は、JLS§14.4のローカル変数宣言ステートメントによって管理されます。

スコープローカル変数が定義されているJLS§6.3、宣言のスコープ:

  • ブロック内のローカル変数宣言のスコープ(§14.4)は、宣言が出現するブロックの残りの部分であり、独自のイニシャライザから始まり、ローカル変数宣言ステートメントの右側にさらに宣言子を含めます。

初期化子は宣言されている変数のスコープ内にあることに注意してください。では、なぜint d = d + 1;コンパイルしないのですか?

その理由は、明確な割り当てに関するJavaの規則によるものです(JLS§16)。明確な割り当ては基本的に、ローカル変数へのすべてのアクセスにはその変数への先行割り当てが必要であり、Javaコンパイラーはループと分岐をチェックして、割り当てが使用の前に常に発生することを確認します(これにより、明確な割り当てには仕様セクション全体が専用になりますそれに)。基本的なルールは:

  • ローカル変数や空白の最終フィールドのすべてのアクセスに関してはxx確かにアクセスする前に割り当てられた、またはコンパイル時エラーが発生しなければなりません。

ではint d = d + 1;、へのアクセスdはローカル変数fineに解決されますが、アクセスdされる前に割り当てられていないためd、コンパイラーはエラーを発行します。その割り当ては、まず起こり、次いで、及び(1)その割り当ての結果に初期化されます。int c = c = 1c = 1cc

明確な割り当てルールのため、ローカル変数宣言int d = (d = 1) + d; (フィールド宣言とは異なりint b = (b = 1) + b)正常コンパイルdされdます。これは、最終に到達するまでに確実に割り当てられるためです。


参照の+1ですが、この表現が間違っていると思います。「int a = a = 1;(b)に違反しているためコンパイルします」、コンパイルしない4つの要件のいずれかに違反している場合。それはしかし、それはしていませんIS(JLSの文言で二重否定はあまりここには役立ちません)代入の左側に。in int b = b + 1bは割り当ての右側(左側ではなく)にあるため、これに違反します...
msam

...よくわからないのは次のとおりです。宣言が割り当ての前にテキストとして表示されない場合、これらの4つの条件が満たされる必要があります。この場合、宣言は割り当ての前に「テキスト」として表示されると思いますint x = x = 1。このいずれも当てはまらない場合。
msam 2013

@msam:少し混乱を招きますが、基本的には、前方参照を行うために4つの条件のいずれかに違反する必要があります。前方参照 4つすべての条件を満たしている場合、それは不正です。
nneonneo 2013

@msam:また、完全な宣言は初期化子の後でのみ有効になります。
nneonneo 2013

@mrfishie:大きな答えですが、Java仕様には驚くほどの深さがあります。問題は表面的に見えるほど単純ではありません。(私はかつてJavaのサブセットのコンパイラーを作成したことがあるので、JLSのさまざまな機能に精通しています)。
nneonneo 2013

86
int x = x = 1;

に相当

int x = 1;
x = x; //warning here

にいる間

int x = x + 1; 

最初に計算する必要x+1がありますが、xの値が不明であるため、エラーが発生します(コンパイラーはxの値が不明であることを認識しています)


4
これに加えて、OpenSauceからの正しい関連性に関するヒントが非常に役立ちました。
TobiMcNamobi 2013

1
割り当ての戻り値は、変数の値ではなく、割り当てられる値であると思いました。
zzzzBov 2013

2
@zzzzBovは正しいです。int x = x = 1;はと同じですがint x = (x = 1)、ではありません x = 1; x = x;。これを行ってもコンパイラの警告は表示されません。
nneonneo 2013

int x = x = 1;s 演算子のx = (x = 1)右結合性のため、int と同等=
Grijesh Chauhan 2013

1
@nneonneoとint x = (x = 1)と等価であるint x; x = 1; x = x;(変数宣言、フィールド初期化子、前記評価結果に変数の割り当ての評価)、したがって警告
MSAM

41

これはおおよそ次と同等です。

int x;
x = 1;
x = 1;

まず、int <var> = <expression>;常にと同等です

int <var>;
<var> = <expression>;

この場合、式はx = 1であり、これもステートメントです。x = 1var xはすでに宣言されているため、これは有効なステートメントです。これは、値1の式でもあり、次にx再度割り当てられます。


わかりましたが、あなたが言うようになったら、なぜクラスのスコープで2番目のステートメントがエラーを出すのでしょうか 0つまり、intのデフォルト値を取得するので、結果はでなく1になるはずundefined referenceです。
Marcin 2013

@izogfifの回答をご覧ください。C ++コンパイラーが変数にデフォルト値を割り当てるため、動作しているように見えます。javaがクラスレベルの変数に対して行うのと同じ方法。
Marcin 2013

@Marcin:Javaでは、intはローカル変数の場合、0に初期化されません。それらがメンバー変数である場合のみ、0に初期化されます。したがって、2行目は初期化されていないx + 1ため、値は定義されていませんx
OpenSauce 2013

1
@OpenSauceしかしはx れるメンバ変数(「クラススコープ」)として定義されます。
Jacob Raihle 2013

@JacobRaihle:ああ、その部分は見つけられなかった。varを0に初期化するためのバイトコードが、明示的な初期化命令があるとコンパイラが生成するかどうかはわかりません。クラスとオブジェクトの初期化について詳しく説明している記事がありますが、この問題に正確に対処しているとは思いません:javaworld.com/jw-11-2001/jw-1102-java101.html
OpenSauce

12

Javaまたは現代の言語では、割り当ては右側から行われます。

2つの変数xとyがあるとします。

int z = x = y = 5;

このステートメントは有効であり、これはコンパイラーがステートメントを分割する方法です。

y = 5;
x = y;
z = x; // which will be 5

しかし、あなたの場合

int x = x + 1;

このように分割されるため、コンパイラは例外を出しました。

x = 1; // oops, it isn't declared because assignment comes from the right.

警告はx = 1でなくx = xにあります
Asim Ghaffar 2013

8

int x = x = 1; 次と等しくない:

int x;
x = 1;
x = x;

javapが再び役に立ちます。これらは、このコード用に生成されたJVM命令です。

0: iconst_1    //load constant to stack
1: dup         //duplicate it
2: istore_1    //set x to constant
3: istore_1    //set x to constant

もっと好き:

int x = 1;
x = 1;

未定義の参照エラーをスローする理由はここにありません。初期化前に変数が使用されるようになったため、このコードは仕様に完全に準拠しています。実際、変数の使用法はまったくなく、割り当てだけです。そして、JITコンパイラーはさらに進んで、そのような構造を排除します。正直言って、このコードがJLSの変数の初期化と使用法の仕様にどのように関連付けられているのか理解できません。使用法は問題ありません。;)

私が間違っている場合は修正してください。多くのJLS段落を参照する他の回答がなぜ多くのプラスを集めているのか、私にはわかりません。これらの段落は、この場合と共通点はありません。シリアル割り当ては2つだけで、それ以上はありません。

私たちが書いた場合:

int b, c, d, e, f;
int a = b = c = d = e = f = 5;

等しい:

f = 5
e = 5
d = 5
c = 5
b = 5
a = 5

ほとんどの式は、再帰なしで変数に1つずつ割り当てられます。変数は好きなように変更できます。

a = b = c = f = e = d = a = a = a = a = a = e = f = 5;

7

ではint x = x + 1;、あなた程度の値が何である、Xに1を追加しx、それがまだ作成されていないが。

しかし、int x=x=1;1をに割り当てるため、エラーなしでコンパイルされxます。


5

最初のコード=には、プラスではなく2番目のコードが含まれています。これはどこでもコンパイルされますが、2番目のコードはどちらの場所でもコンパイルされません。


5

2番目のコードでは、xは宣言の前に使用されますが、最初のコードでは2回だけ割り当てられますが、これは意味がありませんが有効です。


5

一歩一歩分解していきましょう

int x = x = 1

x = 1、変数xに1を割り当てます

int x = x、それ自体にxが何であるかをintとして割り当てます。xは以前は1として割り当てられていたため、冗長な方法ではありますが、1を保持しています。

それはうまくコンパイルされます。

int x = x + 1

x + 1、変数xに1を追加します。ただし、xが未定義の場合、コンパイルエラーが発生します。

int x = x + 1、したがって、この行は、equalsの右側の部分がコンパイルされず、割り当てられていない変数に1が追加されるため、エラーをコンパイルします。


いいえ、=演算子が2つある場合は右結合なので、と同じint x = (x = 1);です。
Jeppe Stig Nielsen

ああ、私の命令はオフです。申し訳ありません。逆方向に実行する必要がありました。私は今それを切り替えました。
steventnorris 2013

3

2つ目int x=x=1は、xに値を割り当てるため、コンパイルですが、他の場合int x=x+1、変数xは初期化されていません。Javaローカル変数がデフォルト値に初期化されていないことを思い出してください。注:int x=x+1クラスのスコープでも()の場合、変数が作成されないため、コンパイルエラーが発生します。


2
int x = x + 1;

警告付きでVisual Studio 2008で正常にコンパイルされます

warning C4700: uninitialized local variable 'x' used`

2
インタレスト。C / C ++ですか?
Marcin 2013

@Marcin:はい、C ++です。@msam:すみません、c代わりにタグを見たと思いますjavaが、どうやらそれは別の質問でした。
izogfif 2013

C ++ではコンパイラがプリミティブ型のデフォルト値を割り当てるため、コンパイルされます。使用するbool y;y==truefalseが返されます。
スリハルシャチラカパティ2013

@SriHarshaChilakapati、それはC ++コンパイラの標準のようなものですか?void main() { int x = x + 1; printf("%d ", x); }Visual Studio 2008でコンパイルすると、Debugで例外が発生Run-Time Check Failure #3 - The variable 'x' is being used without being initialized.し、Releaseで1896199921コンソールに数値が出力されるためです。
izogfif 2013

1
@SriHarshaChilakapati他の言語について話す:C#では、staticフィールド(クラスレベルの静的変数)に対して同じルールが適用されます。たとえばpublic static int x = x + 1;、Visual C#で警告なしにコンパイルすると宣言されたフィールド。おそらくJavaでも同じですか?
Jeppe Stig Nielsen

2

xはx = x + 1;で初期化されていません。

Javaプログラミング言語は静的に型付けされています。つまり、すべての変数は、使用する前に宣言する必要があります。

プリミティブデータ型を参照


3
値を使用する前に変数を初期化する必要性は、静的型付けとは関係ありません。静的型付け:変数の型を宣言する必要があります。使用前に初期化:値を使用するには、その前に値を持っている必要があります。
Jon Bright

@JonBright:変数の型を宣言する必要性も、静的型付けとは関係ありません。たとえば、型推論を持つ静的型付き言語があります。
hammar 2013

@hammar、私がそれを見る方法、あなたはそれを2つの方法で主張することができます:型推論では、システムが推論できる方法で暗黙的に変数の型を宣言しています。または、型推論は3番目の方法であり、変数は実行時に動的に型付けされず、ソースレベルで使用されます。これは、その使用法と推論によって異なります。どちらにしても、ステートメントは真実のままです。しかし、そうです、私は他の型システムについては考えていませんでした。
Jon Bright

2

コードが実際に機能するため、コード行は警告なしでコンパイルされません。コードを実行するとint x = x = 1、Javaはまずx、定義されたとおりに変数を作成します。次に、割り当てコード(x = 1)を実行します。以来、x既に定義されている、システムは、設定エラーを持っていないx1本にそれが現在の値であるので、値1を返しますx。そのため、x最終的に1に設定されます
。Javaは基本的に、次のようにコードを実行します。

int x;
x = (x = 1); // (x = 1) returns 1 so there is no error

ただし、2番目のコードでint x = x + 1は、+ 1ステートメントxを定義する必要がありますが、その時点では定義されていません。代入ステートメントは常にの右側の=コードが最初に実行されることを意味するため、コードxは未定義であるため失敗します。Javaは次のようなコードを実行します。

int x;
x = x + 1; // this line causes the error because `x` is undefined

-1

コンパイラーはステートメントを右から左に読み、その反対を行うように設計しました。それが最初はイライラした理由です。これを習慣にして、ステートメント(コード)を右から左に読むと、このような問題は発生しません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.