510 Stimmen

Warum gerät dies in eine Endlosschleife?

Ich habe den folgenden Code:

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

Wir wissen, dass er einfach hätte schreiben sollen x++ o x=x+1 sondern auf x = x++ sollte es zunächst das Attribut x auf sich selbst zu übertragen und später zu erhöhen. Warum funktioniert x weiter mit 0 als Wert?

--Aktualisierung

Hier ist der Bytecode:

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

}

Ich lese über die Anweisungen um zu versuchen zu verstehen...

8 Stimmen

Ich vermute, dass folgendes passiert: 1. x in ein Register laden (=0); 2. x inkrementieren (x=1); 3. Registerwert in x speichern (x=0). In C/C++ wäre dies ein undefiniertes Verhalten, da es keinen formalen Sequenzpunkt gibt, der die Reihenfolge von 2 und 3 definiert. Hoffentlich kann Ihnen jemand etwas Entsprechendes aus der Java-Spezifikation zitieren.

1 Stimmen

Das ist wirklich interessant, denn auch wenn die x ist zugeordnet zu x zuerst, dann sollte die Inkrementierung gut funktionieren.

19 Stimmen

Wir haben dies in C++ ausprobiert, um zu sehen, was passieren würde, und es gibt 1,2,3 aus und beendet sich. Das hatte ich nicht erwartet. Ich nehme an, dass es compilerabhängig ist, da es ein undefiniertes Verhalten ist. Wir haben den gnu g++ verwendet.

362voto

Dan Tao Punkte 121990

Hinweis : Ursprünglich habe ich den C#-Code in dieser Antwort zur Veranschaulichung gepostet, da C# die Übergabe von int Parameter durch Referenz mit der ref Stichwort. Ich habe mich entschlossen, ihn mit aktuellem, legalem Java-Code zu aktualisieren, der das erste MutableInt Klasse, die ich bei Google gefunden habe, um eine Art Annäherung an das ref in C# tut. Ich kann nicht wirklich sagen, ob das hilft oder schadet die Antwort. Ich werde sagen, dass ich persönlich nicht getan haben, dass alle viel Java-Entwicklung; so für alles, was ich weiß, es könnte viel mehr idiomatische Möglichkeiten, um diesen Punkt zu veranschaulichen sein.


Vielleicht sollten wir eine Methode entwickeln, die dem entspricht, was x++ wird dies deutlicher machen.

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

Oder? Den übergebenen Wert inkrementieren und den ursprünglichen Wert zurückgeben: Das ist die Definition des Postincrement-Operators.

Schauen wir uns nun an, wie sich dieses Verhalten in Ihrem Beispielcode auswirkt:

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

postIncrement(x) macht was? Erhöht x ja. Und dann gibt zurück, was x war vor dem Inkrement . Dieser Rückgabewert wird dann zugewiesen an x .

Daher ist die Reihenfolge der Werte, die den x ist 0, dann 1, dann 0.

Dies wird vielleicht noch deutlicher, wenn wir den obigen Text umformulieren:

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.

Ihre Fixierung auf die Tatsache, dass Sie, wenn Sie die x auf der linken Seite der obigen Zuordnung mit y Die Aussage "Sie können sehen, dass er zunächst x erhöht und später y zuordnet" scheint mir verwirrend. Es ist nicht x die zugewiesen wird y ; es ist der Wert, der zuvor für x . Wirklich, die Injektion y unterscheidet sich nicht von dem obigen Szenario; wir haben einfach etwas:

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.

Es ist also klar: x = x++ ändert den Wert von x nicht. Es bewirkt, dass x immer die Werte x 0 , dann x 0 + 1, und dann x 0 wieder.


Update : Übrigens, damit Sie keine Zweifel haben, dass x jemals "zwischen" der Inkrement-Operation und der Zuweisung im obigen Beispiel auf 1 gesetzt wird, habe ich eine schnelle Demo zusammengestellt, um zu zeigen, dass dieser Zwischenwert tatsächlich "existiert", obwohl er im ausführenden Thread nie "gesehen" wird.

Die Demo ruft auf x = x++; in einer Schleife, während ein separater Thread kontinuierlich den Wert von x an die Konsole.

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);
        }
    }
}

Nachfolgend sehen Sie einen Auszug aus der Ausgabe des obigen Programms. Beachten Sie das unregelmäßige Auftreten von 1en und 0en.

Starting background thread...
0
0
1
1
0
0
0
0
0
0
0
0
0
0
1
0
1

1 Stimmen

Sie müssen keine Klasse erstellen, um eine Referenz in Java zu übergeben (obwohl das sicherlich funktionieren würde). Sie können die Integer Klasse, die Teil der Standardbibliothek ist, und sie hat sogar den Vorteil, dass sie automatisch in und aus int fast transparent.

3 Stimmen

@rmeador Integer ist unveränderlich, also kann man seinen Wert nicht ändern. AtomicInteger hingegen ist veränderbar.

0 Stimmen

@rmeador: Ich frage, weil ich es ehrlich gesagt nicht weiß, und nicht, um Sie herauszufordern: Wie würde das funktionieren, was Sie vorschlagen? Was ich meine, ist, wie können Sie einen neuen Wert zuweisen x innerhalb einer Methode, auch wenn sie es verpackt als Integer ? (Ist die Integer Typ nicht unveränderlich?) Ich weiß, dass man in .NET, zum Beispiel mit C#, eine int como object aber Sie würden trotzdem nicht in der Lage sein zuweisen. einen neuen Wert für die Variable, die diese int innerhalb einer Methode, es sei denn, sie wurde als ref Parameter.

175voto

axtavt Punkte 233070

x = x++ funktioniert auf folgende Weise:

  • Zunächst wertet er den Ausdruck x++ . Die Auswertung dieses Ausdrucks ergibt einen Ausdruckswert (der dem Wert von x vor Inkrement) und Inkremente x .
  • Später weist es den Wert des Ausdrucks x und überschreibt den inkrementierten Wert.

Die Abfolge der Ereignisse sieht also wie folgt aus (es handelt sich um einen tatsächlich dekompilierten Bytecode, wie er von javap -c (mit meinen Anmerkungen):

   8:   iload\_1         // Remember current value of x in the stack
   9:   iinc    1, 1    // Increment x (doesn't change the stack)
   12:  istore\_1        // Write remebered value from the stack to x

Zum Vergleich, x = ++x :

   8:   iinc    1, 1    // Increment x
   11:  iload\_1         // Push value of x onto stack
   12:  istore\_1        // Pop value from the stack to x

0 Stimmen

Wenn Sie einen Test machen, können Sie sehen, dass es zuerst erhöht und später Attribute. So sollte es nicht Attribut Null.

2 Stimmen

@Tom, das ist der Punkt, aber - weil das alles eine einzige Sequenz ist, tut es Dinge in einer nicht offensichtlichen (und wahrscheinlich undefinierten) Reihenfolge. Wenn Sie versuchen, dies zu testen, fügen Sie einen Sequenzpunkt hinzu und erhalten ein anderes Verhalten.

0 Stimmen

Bezüglich Ihrer Bytecode-Ausgabe: Beachten Sie, dass iinc inkrementiert eine Variable, nicht aber einen Stack-Wert und hinterlässt auch keinen Wert auf dem Stack (im Gegensatz zu fast jeder anderen arithmetischen Operation). Sie können den Code hinzufügen, der von ++x zum Vergleich.

106voto

codaddict Punkte 426877

Dies geschieht, weil der Wert von x wird überhaupt nicht inkrementiert.

x = x++;

ist gleichbedeutend mit

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

Erläuterung:

Schauen wir uns den Bytecode für diesen Vorgang an. Betrachten wir eine Beispielklasse:

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

Wenn wir nun den Klassendisassembler darauf anwenden, erhalten wir:

$ 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
}

Jetzt ist die Java-VM ist stapelbasiert, d.h. für jede Operation werden die Daten auf den Stapel geschoben und vom Stapel werden die Daten herausgepoppt, um die Operation durchzuführen. Außerdem gibt es eine weitere Datenstruktur, in der Regel ein Array zur Speicherung der lokalen Variablen. Den lokalen Variablen werden IDs zugewiesen, die lediglich die Indizes für das Array darstellen.

Schauen wir uns die Mnemonics sur main() Methode:

  • iconst_0 : Der konstante Wert 0 wird auf den Stapel geschoben.
  • istore_1 : Das oberste Element des Stapels wird herausgepoppt und in der lokalen Variablen mit dem Index 1
    die ist x .
  • iload_1 : Der Wert an der Standort 1 das ist der Wert von x das ist 0 wird in den Stapel geschoben.
  • iinc 1, 1 : Der Wert an der Speicherplatz 1 wird inkrementiert um 1 . Also x wird nun 1 .
  • istore_1 : Der Wert an der Spitze der des Stapels wird an der Speicherstelle 1 . Das ist 0 zugeordnet ist zugewiesen. x Überschreiben seinen inkrementierten Wert.

Daher ist der Wert von x ändert sich nicht, was zu einer Endlosschleife führt.

6 Stimmen

Tatsächlich wird sie inkrementiert (das ist die Bedeutung von ++ ), aber die Variable wird später überschrieben.

10 Stimmen

int temp = x; x = x + 1; x = temp; Es ist besser, wenn Sie in Ihrem Beispiel keine Tautologie verwenden.

54voto

Jaydee Punkte 4108
  1. Bei der Präfix-Notation wird die Variable erhöht, BEVOR der Ausdruck ausgewertet wird.
  2. Die Postfix-Notation wird NACH der Auswertung des Ausdrucks hochgezählt.

Aber " = " hat eine niedrigere Operatorpriorität als " ++ ".

だから x=x++; sollte wie folgt ausgewertet werden

  1. x für den Einsatz vorbereitet (bewertet)
  2. x inkrementiert
  3. Vorheriger Wert von x zugewiesen x .

0 Stimmen

Dies ist die beste Antwort. Mit etwas Markup wäre es etwas mehr herausgehoben worden.

1 Stimmen

Das ist falsch. Es geht nicht um Präzedenzfälle. ++ hat einen höheren Vorrang als = in C und C++, aber die Anweisung ist in diesen Sprachen undefiniert.

3 Stimmen

Die ursprüngliche Frage bezieht sich auf Java

35voto

Robert Munteanu Punkte 64955

Keine der Antworten war so richtig, also fange ich mal an:

Wenn Sie schreiben int x = x++ ordnen Sie nicht zu x selbst der neue Wert sein soll, weisen Sie x zum Rückgabewert der Methode x++ Ausdruck. Dies ist zufällig der ursprüngliche Wert von x wie angedeutet in Colin Cochrane's Antwort .

Testen Sie zum Spaß den folgenden Code:

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

Das Ergebnis wird sein

0
1

Der Rückgabewert des Ausdrucks ist der Anfangswert von x die gleich Null ist. Aber später, beim Lesen des Wertes von x erhalten wir den aktualisierten Wert, also eins.

1 Stimmen

Ich werde versuchen, die Bytecode-Zeilen zu verstehen, siehe mein Update, so dass es klar sein wird :)

1 Stimmen

Die Verwendung von println() war für mich sehr hilfreich, um dies zu verstehen.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X