なぜこれが無限ループになるのですか?


493

私は次のコードを持っています:

public class Tests {
    public static void main(String[] args) throws Exception {
        int x = 0;
        while(x<3) {
            x = x++;
            System.out.println(x);
        }
    }
}

私たちは、彼がちょうど書かれている必要があります知っているx++x=x+1、しかしにx = x++それべき最初の属性x自体に、後でそれをインクリメントします。なぜ価値xとして続けるの0ですか?

- 更新

バイトコードは次のとおりです。

public class Tests extends java.lang.Object{
public Tests();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   return

public static void main(java.lang.String[])   throws java.lang.Exception;
  Code:
   0:   iconst_0
   1:   istore_1
   2:   iload_1
   3:   iconst_3
   4:   if_icmpge   22
   7:   iload_1
   8:   iinc    1, 1
   11:  istore_1
   12:  getstatic   #2; //Field java/lang/System.out:Ljava/io/PrintStream;
   15:  iload_1
   16:  invokevirtual   #3; //Method java/io/PrintStream.println:(I)V
   19:  goto    2
   22:  return

}

理解しようとするための指示について読みます...


8
私は何が起こっているのかと思います:1. xをレジスターにロードします(= 0)。2.インクリメントx(x = 1); 3.レジスタ値をx(x = 0)に保存します。C / C ++では、2と3の順序を定義する正式なシーケンスポイントがないため、これは未定義の動作になります。うまくいけば、誰かがJava仕様から同等のものを引用できるでしょう。
Rupの

19
何が起こるかを確認するためにC ++でこれを試し、1、2、3を出力して終了します。それを予想しなかった。未定義の動作なので、コンパイラに依存していると思います。私たちはgnu g ++を使用しました。
悲しむ

13
@saj x++はポストインクリメントです。結果のx=割り当てです。結果のは、元である(そして増加の副作用があり、それは結果を変更しない)、これはのように解釈できるように、x++xvar tmp = x; x++; x = tmp;
マルクGravell

5
よくある質問ですが、残念ですが、正しい答えを選んだ後でも(繰り返し)答えが止まりません。私の「最近の活動」画面は同じ答えでいっぱいです、そしてもっと来ます...
トムブリト

3
@Rob Vermeulenあなたはコメントをする前に完全な質問を読みたくなるかもしれません..;)これは私の学生によって作られたコードであり、私はこの振る舞いの理由に興味を持っていました。
トムブリト

回答:


354

:C#ではキーワードをint使用して参照でパラメーターを渡すことができるため、元々は説明のためにこの回答にC#コードを投稿しましたref。私はMutableInt、Googleで最初に見つけたクラスを使用して実際の合法的なJavaコードで更新することに決めましたref。それが答えを助けるか傷つけるかどうか私は本当にわかりません。私は個人的にはそれほど多くのJava開発を行っていません。したがって、この点を説明するためのもっと慣用的な方法が存在する可能性があることを私は知っています。


おそらく、それと同等の処理を行うメソッドを作成するx++と、これがより明確になります。

public MutableInt postIncrement(MutableInt x) {
    int valueBeforeIncrement = x.intValue();
    x.add(1);
    return new MutableInt(valueBeforeIncrement);
}

正しい?渡された値をインクリメントし、元の値を返します。これがポストインクリメント演算子の定義です。

次に、この動作がサンプルコードでどのように実行されるかを見てみましょう。

MutableInt x = new MutableInt();
x = postIncrement(x);

postIncrement(x)何をしますか?増分x、はい。そして、何を返しx 増分の前に。次に、この戻り値がに割り当てられxます。

したがって、割り当てられる値の順序は0、1、0の順になりますx

上記を書き直すと、これはさらに明確になる可能性があります。

MutableInt x = new MutableInt();    // x is 0.
MutableInt temp = postIncrement(x); // Now x is 1, and temp is 0.
x = temp;                           // Now x is 0 again.

x上記の割り当ての左側をで置き換えるとy、「最初にxがインクリメントされ、後でyに割り当てられることがわかります」という固執は、私を混乱させます。にx割り当てられているわけではありませんy。それは、以前に割り当てられた値x。本当に、注入yは上記のシナリオと何の違いもありません。私たちは単に持っています:

MutableInt x = new MutableInt();    // x is 0.
MutableInt y = new MutableInt();    // y is 0.
MutableInt temp = postIncrement(x); // Now x is 1, and temp is 0.
y = temp;                           // y is still 0.

ですからx = x++、xの値は実質的に変更されません。これにより、xの値は常にx 0、次にx 0 + 1、次にx 0になります。


更新:ちなみに、xインクリメント操作と上記の例の割り当ての「間に」1が割り当てられることを疑わないように、この中間値が実際に「存在する」ことを示す簡単なデモを一緒に示しましたが、実行中のスレッドで「見える」ことはありません。

デモはx = x++;ループで呼び出され、別のスレッドが継続的にの値をxコンソールに出力します。

public class Main {
    public static volatile int x = 0;

    public static void main(String[] args) {
        LoopingThread t = new LoopingThread();
        System.out.println("Starting background thread...");
        t.start();

        while (true) {
            x = x++;
        }
    }
}

class LoopingThread extends Thread {
    public @Override void run() {
        while (true) {
            System.out.println(Main.x);
        }
    }
}

以下は、上記のプログラムの出力の抜粋です。1と0の両方の不規則な発生に注意してください。

バックグラウンドスレッドを開始しています...
0
0
1
1
0
0
0
0
0
0
0
0
0
0
1
0
1

1
Javaで参照渡しするクラスを作成する必要はありません(確かに機能します)。Integer標準ライブラリの一部であるクラスを使用でき、int ほとんど透過的にオートボックス化されるという利点もあります。
rmeador

3
@rmeador Integerは不変なので、値を変更することはできません。ただし、AtomicIntegerは変更可能です。
ILMTitan 2010

5
@Dan:ちなみに、x最後の例ではを宣言する必要があります。宣言volatileしないと、未定義の動作であり、1sの表示は実装固有です。
axtavt

4
@burkestar:私は、そのリンクがされたとは思わない、かなり動作が実際にC ++で定義されていません(私が間違ってる場合を除く)、それは、Javaの質問とだから、この場合に適切な。
Dan Tao

5
@Tom Brito-Cでは定義されていません... 割り当ての前または後に行う++ ことできます。実際には、Javaと同じことをするコンパイラがあるかもしれませんが、それには賭けたくないでしょう。
detly

170

x = x++ 次のように動作します:

  • まず、式を評価しますx++。この式を評価すると、式の値(xインクリメント前の値)とインクリメントが生成されますx
  • その後、式の値をに割り当て、x増分した値を上書きします。

したがって、イベントのシーケンスは次のようになります(これは、によって生成された実際の逆コンパイルされたバイトコードjavap -cで、コメントが含まれています)。

   8:iload_1 //スタック内のxの現在の値を記憶する
   9:iinc 1、1 //インクリメントx(スタックを変更しない)
   12:istore_1 //記憶された値をスタックからxに書き込む

、比較のためにx = ++x

   8:iinc 1、1 //インクリメントx
   11:iload_1 // xの値をスタックにプッシュします
   12:istore_1 //スタックからxに値をポップします

テストを行うと、最初にインクリメントされ、その後に属性が表示されます。そのため、ゼロと見なすべきではありません。
トム・ブリト

2
@Tomがポイントですが、これはすべて1つのシーケンスなので、明白ではない(おそらく未定義の)順序で処理を行っています。これをテストすることで、シーケンスポイントが追加され、異なる動作が得られます。
Rupの

バイトコード出力について:iinc変数をインクリメントしても、スタック値はインクリメントせず、(ほとんどすべての算術演算とは異なり)スタックに値を残さないことに注意してください。++x比較のためにによって生成されたコードを追加したい場合があります。
Anon、2009

3
@Rep CまたはC ++では定義されていない可能性がありますが、Javaでは適切に定義されています。
ILMTitan 2010


104

これは、の値xがまったく増分されないために発生します。

x = x++;

に相当

int temp = x;
x++;
x = temp;

説明:

この操作のバイトコードを見てみましょう。サンプルクラスについて考えてみましょう。

class test {
    public static void main(String[] args) {
        int i=0;
        i=i++;
    }
}

これでクラス逆アセンブラを実行すると、次のようになります。

$ javap -c test
Compiled from "test.java"
class test extends java.lang.Object{
test();
  Code:
   0:    aload_0
   1:    invokespecial    #1; //Method java/lang/Object."<init>":()V
   4:    return

public static void main(java.lang.String[]);
  Code:
   0:    iconst_0
   1:    istore_1
   2:    iload_1
   3:    iinc    1, 1
   6:    istore_1
   7:    return
}

これでJava VMはスタックベースになり、各操作でデータがスタックにプッシュされ、スタックからデータがポップアウトして操作を実行します。また、別のデータ構造もあります。通常は、ローカル変数を格納するための配列です。ローカル変数には、配列のインデックスであるIDが与えられます。

メソッドのニーモニックを見てみましょうmain()

  • iconst_0:定数値0 がスタックにプッシュされます。
  • istore_1:スタックの一番上の要素がポップアウトされ、インデックスで1
    あるローカル変数に格納されます x
  • iload_1:位置での値1の値でx あるが0、スタックにプッシュされます。
  • iinc 1, 1:メモリ位置の値1はによって増分され1ます。だからx今になり 1ます。
  • istore_1:スタックの最上位の値がメモリ位置に保存されます1。これは、増分された値0x 上書きするために割り当てられます。

したがって、の値はx変化せず、無限ループになります。


5
実際にはインクリメントされます(つまりの意味です++)が、変数は後で上書きされます。
Progman

10
int temp = x; x = x + 1; x = temp;あなたの例ではトートロジーを使用しない方が良いでしょう。
スコットチェンバレン

52
  1. プレフィックス表記は、式が評価される前に変数をインクリメントします。
  2. Postfix表記は、式の評価後に増加します。

ただし、「=」は「」よりも演算子の優先順位が低くなってい++ます。

したがってx=x++;、次のように評価する必要があります

  1. x 割り当ての準備(評価済み)
  2. x 増加した
  3. x割り当てられたの以前の値x

これが最良の答えです。いくつかのマークアップは、それをもう少し目立たせるのに役立ちました。
ジャスティンフォース

1
これは間違っています。優先順位についてではありません。 CおよびC ++ ++よりも優先順位は高く=なりますが、ステートメントはこれらの言語では定義されていません。
Matthew Flaschen、

2
元の質問はJavaに関するものです
Jaydee

34

正解はどこにもないので、ここに行きます:

を記述しているときはint x = x++xそれ自体が新しい値になるように割り当てるのではなくxx++式の戻り値になるように割り当てます。これはxColin Cochraneの回答に示唆されているように、の元の値です。

楽しみのために、次のコードをテストします。

public class Autoincrement {
        public static void main(String[] args) {
                int x = 0;
                System.out.println(x++);
                System.out.println(x);
        }
}

結果は

0
1

式の戻り値は、xゼロの初期値です。しかし、後で、の値を読み取るときにx、更新された値、つまり1を受け取ります。


私はバイトコード行を理解しようとします、私の更新を見て、それでそれが明確になるでしょう。:)
Tom Brito

これを理解するには、println()を使用することが非常に役立ちました。
ErikE 2010

29

それはすでに他の人によってよく説明されています。関連するJava仕様セクションへのリンクのみを含めます。

x = x ++は式です。Javaは評価順序に従います。最初に式x ++を評価し、x をインクリメントして、結果の値を以前のxの値に設定します。次に、式の結果を変数xに割り当てます。最後に、xは以前の値に戻ります。


1
+1。これは、「なぜ?」という実際の質問に対する最善の答えです。
Matthew Flaschen

18

この文:

x = x++;

このように評価します:

  1. xスタックにプッシュします。
  2. 増分x;
  3. xスタックからポップします。

したがって、値は変更されません。それと比較してください:

x = ++x;

これは次のように評価されます:

  1. 増分x;
  2. xスタックにプッシュします。
  3. xスタックからポップします。

あなたが欲しいのは:

while (x < 3) {
  x++;
  System.out.println(x);
}

13
間違いなく正しい実装ですが、問題は「なぜですか」です。
p.campbell 2010

1
元のコードは、xでポストインクリメントを使用し、それをxに割り当てていました。xはインクリメントの前にxにバインドされるため、値が変更されることはありません。
wkl 2010

5
@cletus私は反対投票者ではありませんが、最初の回答には説明が含まれていませんでした。それはdo 'x ++ `とだけ言った。
Petar Minchev

4
@cletus:私は反対票を投じませんでしたが、あなたの答えはもともと単なるx++コードスニペットでした。
p.campbell 2010

10
説明も間違っています。コードが最初にxをxに割り当て、次にxをインクリメントした場合、正常に機能します。x++;ソリューションをに変更するだけでx=x; x++;、元のコードが実行していると主張していることを実行できます。
Wooble

10

答えはかなり簡単です。物事が評価される順序と関係があります。x++値を返し、x増分しますx

したがって、式の値x++0です。したがってx=0、ループ内で毎回割り当てています。確かにx++この値は増加しますが、それは割り当ての前に起こります。


1
うわー、答えが短くて単純な場合、つまりこの答えは、このページに非常に多くの詳細があります。
Charles Goodwin

8

http://download.oracle.com/javase/tutorial/java/nutsandbolts/op1.htmlから

インクリメント/デクリメント演算子は、オペランドの前(前置)または後(後置)に適用できます。コードresult ++; および++ result; 両方とも結果的に1ずつ増加します。唯一の違いは、接頭辞バージョン(++ result)は増分値に評価されるのに対し、postfixバージョン(結果++)は元の値に評価されることです。単純なインクリメント/デクリメントを実行しているだけの場合、どちらのバージョンを選択してもかまいません。ただし、より大きな式の一部でこの演算子を使用する場合、選択する式によって大きな違いが生じる可能性があります。

説明のために、次のことを試してください。

    int x = 0;
    int y = 0;
    y = x++;
    System.out.println(x);
    System.out.println(y);

1と0を出力します。


1
問題となるのは評価結果ではなく、店舗の順番です。
Rupの

2
同意しません。X = 0は、次に、X ++は0が返された場合したがって、X = X ++は、x = 0になります
コリンコクラン

ラップはこれについて正しいです。この特定のケースで問題となっているのは、ストアの順序です。y = x ++はx = x ++と同じではありません。後者の場合、xには同じ式で2つの値が割り当てられます。左側のxには、式x ++の評価結果である0 が割り当てられています。右側のxは、1にインクリメントされています。この2つの割り当てが発生する順序が問題です。以前の投稿から、これが機能する方法は次のとおりです:eval = x ++ => eval == 0:右にインクリメントx => x == 1:左x = eval => x == 0
Michael Ekoka

7

次の動作を効果的に実行しています。

  1. 右側の「結果」としてxの値(0)を取得
  2. xの値をインクリメントします(xは1になりました)
  3. 右側(0として保存された)の結果をxに割り当てます(xは0になりました)

ポストインクリメント演算子(x ++)は、問題の変数をインクリメントしてから、その変数を使用する方程式で使用するための値を返します。

編集:コメントのために少し追加します。次のように考えてください。

x = 1;        // x == 1
x = x++ * 5;
              // First, the right hand side of the equation is evaluated.
  ==>  x = 1 * 5;    
              // x == 2 at this point, as it "gave" the equation its value of 1
              // and then gets incremented by 1 to 2.
  ==>  x = 5;
              // And then that RightHandSide value is assigned to 
              // the LeftHandSide variable, leaving x with the value of 5.

わかりましたが、ステップ2と3の順序は何で指定されていますか?
Rupの

@Rup-言語がそれを定義します。方程式の右側が最初に評価され(この場合は "x ++")、結果は左側の変数に割り当てられます。それが言語が機能する方法です。方程式のxを「返す」「x ++」に関する限り、それが後置インクリメント演算子が機能する方法です(xの値を返し、次にインクリメントします)。それが「--x」だった場合は、(xをインクリメントしてから値を返します)。そこに戻るのは正しい言葉ではありませんが、あなたは考えを理解します。
RHSeeger 2010

7

何が起こっているのかを理解するために、マシンコードは実際には必要ありません。

定義によると:

  1. 代入演算子は右辺式を評価し、それを一時変数に格納します。

    1.1。xの現在の値がこの一時変数にコピーされます

    1.2。xが増加します。

  2. 次に、一時変数が式の左側にコピーされます。これは偶然xです。そのため、xの古い値が再びそれ自体にコピーされます。

とてもシンプルです。


5

これは、この場合は増分されないためです。x++この場合、次のようにインクリメントする前に、最初にその値を使用します。

x = 0;

しかし、++x;これを行うと、増加します。


テストを行うと、最初にインクリメントされ、その後に属性が表示されます。そのため、ゼロと見なすべきではありません。
トム・ブリト

1
@トム:私の答えを見てください-x ++が実際にxの古い値を返すことをテストで示します。それが壊れるところです。
ロバートムンテアヌ

「テストを行う場合」-Cで書かれたテストは、Javaが何をするかを教えてくれるのに、Cが何をするかさえわからないと考える人もいるようです。
ジムバルター2016

3

の値は0であるため、値x++は0のままです。この場合、の値xが増加してもしなくても、割り当てx=0が実行されます。これにより、一時的に増加した値x(「非常に短い時間」の場合は1でした)が上書きされます。


しかし、x ++は後処理です。したがって、割り当てが完了した後、xをインクリメントする必要があります。
Sagar V

1
@Sagar V:x++割り当て全体ではなく、式のみx=x++;
Progman

いいえ、割り当てで使用されるxの値が読み取られた後にのみ、インクリメントする必要があると思います。
Rupの

1

これは、他の人が期待する方法で機能します。接頭辞と接尾辞の違いです。

int x = 0; 
while (x < 3)    x = (++x);

1

x ++は、インクリメントのXを「返す」関数呼び出しと考えてください(そのため、ポストインクリメントと呼ばれています)。

したがって、操作の順序は次のとおりです
。1:xの値をキャッシュしてからインクリメントする
2:xをインクリメントする
3:キャッシュされた値を返す(xがインクリメントされる前)
4:戻り値がxに割り当てられる


わかりましたが、ステップ3と4の順序は何で指定されていますか?
Rupの

「インクリメント前のXを返します」が間違っています。私のアップデートを参照してください
Tom Brito

実際には、ステップ3と4は別々の操作ではありません。値を返すのは実際には関数呼び出しではなく、そのように考えるのに役立ちます。割り当てがある場合は常に右側が「評価」され、結果が左側に割り当てられます。評価結果は、操作の順序を理解するのに役立つため、戻り値と考えることができますが、実際には。
jhabbott 09/10/30

おっと、本当です。私はステップ2と4を意味しました-なぜ返された値はインクリメントされた値の上に保存されるのですか?
Rup

1
これは、割り当て演算の定義の一部です。最初に右側が完全に評価され、次に結果が左側に割り当てられます。
jhabbott 2010

1

++がrhsにある場合、数値が増分される前に結果が返されます。++ xに変更すれば問題ありません。Javaは、増分ではなく単一の操作(xからxへの割り当て)を実行するようにこれを最適化します。


1

私が見る限り、インクリメント前の値でインクリメントされた値をオーバーライドする割り当てが原因で、エラーが発生します。つまり、インクリメントが元に戻されます。

具体的には、「x ++」式は、インクリメント後に「x」の値を持つ「++ x」とは対照的に、インクリメント前に「x」の値を持っています。

バイトコードの調査に関心がある場合は、問題の3行を見てみましょう。

 7:   iload_1
 8:   iinc    1, 1
11:  istore_1

7:iload_1#2番目のローカル変数の値をスタックに入れます
8:iinc 1,1#2番目のローカル変数を1だけインクリメントします。これにより、スタックはそのままになります。
9:istore_1#スタックの最上部をポップし、この要素の値を2番目のローカル変数に保存します
(各JVM命令の効果はここで読むことができます

このため、上記のコードは無限にループしますが、++ xを含むバージョンではループしません。++ xのバイトコードはかなり異なるはずです。1年以上前に書いた1.3 Javaコンパイラーから覚えている限り、バイトコードは次のようになります。

iinc 1,1
iload_1
istore_1

したがって、最初の2行を交換するだけで、インクリメント後にスタックの一番上に残った値(つまり、式の「値」)がインクリメント後の値になるようにセマンティクスを変更します。


1
    x++
=: (x = x + 1) - 1

そう:

   x = x++;
=> x = ((x = x + 1) - 1)
=> x = ((x + 1) - 1)
=> x = x; // Doesn't modify x!

一方

   ++x
=: x = x + 1

そう:

   x = ++x;
=> x = (x = x + 1)
=> x = x + 1; // Increments x

もちろん、最終的な結果は、1 x++;つだけの場合と同じ++x;です。



0

これの動作を正確に定義する何かがJava仕様にあるのだろうか。(そのステートメントの明らかな意味は、私がチェックするのが面倒すぎるということです。)

トムのバイトコードから、重要な行は7、8、11であることに注意してください。行7はxを計算スタックにロードします。行8はxを増分します。行11は、スタックからxに戻る値を格納します。自分自身に値を割り当てない通常のケースでは、ロード、保存、およびインクリメントできなかった理由はないと思います。同じ結果が得られます。

同様に、次のように記述した通常のケースがあるとします。z=(x ++)+(y ++);

それが言ったかどうか(技術をスキップする疑似コード)

load x
increment x
add y
increment y
store x+y to z

または

load x
add y
store x+y to z
increment x
increment y

無関係である必要があります。どちらの実装も有効であるべきだと思います。

この振る舞いに依存するコードを書くことについては、私は非常に慎重になるでしょう。それは、実装に依存している、仕様間での仕様に非常によく似ています。ここでの例のようにクレイジーなことをした場合、または2つのスレッドを実行していて、式内の評価の順序に依存していた場合のみ、違いが生じます。



0

x++式があると評価しますx++一部は後の値に影響を与えた評価ではなく、後のステートメント。これx = x++を効果的に翻訳されます

int y = x; // evaluation
x = x + 1; // increment part
x = y; // assignment

0

値を1つインクリメントする前に、値が変数に割り当てられます。


0

それはポストインクリメントされているので起こっています。つまり、式が評価された後に変数がインクリメントされます。

int x = 9;
int y = x++;

xは10になりましたが、yは9で、インクリメントされる前のxの値です。

詳細については、Post Incrementの定義を参照してください。


1
あなたのx/ yの例では、実際のコードとは異なり、その差が関連しています。あなたのリンクはJavaについてさえ言及していません。それは言語の2について言及、問題の文が定義されていません。
Matthew Flaschen

0

以下のコードを確認してください、

    int x=0;
    int temp=x++;
    System.out.println("temp = "+temp);
    x = temp;
    System.out.println("x = "+x);

出力は、

temp = 0
x = 0

post increment、値を増分し、増分前の値を返すことを意味します。そのため、値temp0です。ではtemp = i、ループ内にある場合(コードの最初の行を除く)はどうでしょうか。質問のように!!!!


-1

増分演算子は、割り当て先と同じ変数に適用されます。それは問題を求めています。このプログラムの実行中にx変数の値を確認できると思います。これにより、ループが終了しない理由が明確になります。

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