Warum wird dieser Java-Code kompiliert?

96

Im Methoden- oder Klassenbereich wird die folgende Zeile kompiliert (mit Warnung):

int x = x = 1;

Im Klassenbereich, in dem Variablen ihre Standardwerte erhalten , gibt der folgende Fehler "undefinierte Referenz" aus:

int x = x + 1;

Ist es nicht das erste Mal x = x = 1, dass der Fehler "undefinierte Referenz" auftritt? Oder sollte die zweite Zeile int x = x + 1kompiliert werden? Oder fehlt mir etwas?

Marcin
quelle
1
Wenn Sie das Schlüsselwort wie staticin der Variablen class-scope hinzufügen static int x = x + 1;, erhalten Sie denselben Fehler? Denn in C # macht es einen Unterschied, ob es statisch oder nicht statisch ist.
Jeppe Stig Nielsen
static int x = x + 1schlägt in Java fehl.
Marcin
1
in c # beide int a = this.a + 1;und int b = 1; int a = b + 1;im Klassenbereich (beide sind in Java in Ordnung) schlagen fehl, wahrscheinlich aufgrund von §17.4.5.2 - "Ein Variableninitialisierer für ein Instanzfeld kann nicht auf die erstellte Instanz verweisen." Ich weiß nicht, ob es explizit irgendwo erlaubt ist, aber statisch hat keine solche Einschränkung. In Java sind die Regeln unterschiedlich und static int x = x + 1int x = x + 1
schlagen
Diese Antwort mit einem Bytecode löscht alle Zweifel.
Rgripper

Antworten:

101

tl; dr

Für Felder , int b = b + 1ist illegal , weil bein illegaler Vorwärtsverweis auf ist b. Sie können dies tatsächlich durch Schreiben beheben int b = this.b + 1, das ohne Beschwerden kompiliert wird.

Für lokale Variablen , int d = d + 1ist illegal , weil dnicht vor der Verwendung initialisiert. Dies ist nicht der Fall bei Feldern, die immer standardmäßig initialisiert werden.

Sie können den Unterschied erkennen, indem Sie versuchen, zu kompilieren

int x = (x = 1) + x;

als Felddeklaration und als lokale Variablendeklaration. Ersteres wird scheitern, letzteres wird jedoch aufgrund der unterschiedlichen Semantik erfolgreich sein.

Einführung

Zunächst einmal sind die Regeln für Feld- und lokale Variableninitialisierer sehr unterschiedlich. Diese Antwort wird also die Regeln in zwei Teilen behandeln.

Wir werden dieses Testprogramm durchgehend verwenden:

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

Die Deklaration von bist ungültig und schlägt mit einem illegal forward referenceFehler fehl .
Die Deklaration von dist ungültig und schlägt mit einem variable d might not have been initializedFehler fehl .

Die Tatsache, dass diese Fehler unterschiedlich sind, sollte darauf hinweisen, dass auch die Gründe für die Fehler unterschiedlich sind.

Felder

Feldinitialisierer in Java unterliegen JLS §8.3.2 , Initialisierung von Feldern.

Der Umfang eines Feldes ist in JLS §6.3 , Umfang einer Erklärung definiert.

Relevante Regeln sind:

  • Der Umfang einer Deklaration eines Mitglieds m, das in einem Klassentyp C deklariert oder von diesem geerbt wurde (§8.1.6), umfasst den gesamten Hauptteil von C, einschließlich aller verschachtelten Typdeklarationen.
  • Initialisierungsausdrücke für Instanzvariablen können den einfachen Namen jeder statischen Variablen verwenden, die in der Klasse deklariert oder von dieser geerbt wurde, auch einer, deren Deklaration später in Textform erfolgt.
  • Die Verwendung von Instanzvariablen, deren Deklarationen nach der Verwendung in Textform angezeigt werden, ist manchmal eingeschränkt, obwohl diese Instanzvariablen im Gültigkeitsbereich liegen. In §8.3.2.3 finden Sie die genauen Regeln für die Vorwärtsreferenz auf Instanzvariablen.

§8.3.2.3 sagt:

Die Deklaration eines Mitglieds muss in Textform angezeigt werden, bevor sie nur verwendet wird, wenn das Mitglied ein Instanzfeld (bzw. ein statisches Feld) einer Klasse oder Schnittstelle C ist und alle folgenden Bedingungen erfüllt sind:

  • Die Verwendung erfolgt in einem instanziellen (bzw. statischen) Variableninitialisierer von C oder in einem instanziellen (bzw. statischen) Initialisierer von C.
  • Die Verwendung befindet sich nicht auf der linken Seite einer Aufgabe.
  • Die Verwendung erfolgt über einen einfachen Namen.
  • C ist die innerste Klasse oder Schnittstelle, die die Verwendung einschließt.

Sie können tatsächlich auf Felder verweisen, bevor sie deklariert wurden, außer in bestimmten Fällen. Diese Einschränkungen sollen Code wie verhindern

int j = i;
int i = j;

vom Kompilieren. In der Java-Spezifikation heißt es: "Die oben genannten Einschränkungen dienen dazu, zum Zeitpunkt der Kompilierung zirkuläre oder anderweitig fehlerhafte Initialisierungen abzufangen."

Worauf laufen diese Regeln eigentlich hinaus?

Kurz gesagt, die Regeln besagen grundsätzlich, dass Sie ein Feld vor einer Referenz auf dieses Feld deklarieren müssen , wenn (a) sich die Referenz in einem Initialisierer befindet, (b) die Referenz nicht zugewiesen ist, (c) die Referenz a ist einfacher Name (keine Qualifikationsmerkmale wiethis. ) und (d) es wird nicht innerhalb einer inneren Klasse zugegriffen. Eine Vorwärtsreferenz, die alle vier Bedingungen erfüllt, ist unzulässig, aber eine Vorwärtsreferenz, die bei mindestens einer Bedingung fehlschlägt, ist in Ordnung.

int a = a = 1;Kompiliert, weil es gegen (b) verstößt: Die Referenz a wird zugewiesen, daher ist es legal, avor der avollständigen Erklärung darauf zu verweisen .

int b = this.b + 1Kompiliert auch, weil es gegen (c) verstößt: Die Referenz this.bist kein einfacher Name (mit dem sie qualifiziert ist this.). Dieses seltsame Konstrukt ist immer noch perfekt definiert, weilthis.b es den Wert Null hat.

Grundsätzlich verhindern die Einschränkungen für Feldreferenzen in Initialisierern, dass int a = a + 1sie erfolgreich kompiliert werden.

Beachten Sie, dass die Felddeklaration int b = (b = 1) + bwird nicht kompilieren, weil die letzte bnoch eine illegale vorwärts Referenz.

Lokale Variablen

Deklarationen für lokale Variablen unterliegen JLS §14.4 , Deklarationen für lokale Variablen.

Der Geltungsbereich einer lokalen Variablen ist in JLS §6.3 , Geltungsbereich einer Deklaration definiert:

  • Der Umfang einer lokalen Variablendeklaration in einem Block (§14.4) ist der Rest des Blocks, in dem die Deklaration angezeigt wird, beginnend mit einem eigenen Initialisierer und einschließlich aller weiteren Deklaratoren rechts in der lokalen Variablendeklarationsanweisung.

Beachten Sie, dass Initialisierer im Bereich der deklarierten Variablen liegen. Warum also nicht int d = d + 1;kompilieren?

Der Grund liegt in Javas Regel zur endgültigen Zuweisung ( JLS §16 ). Die eindeutige Zuweisung besagt grundsätzlich, dass jeder Zugriff auf eine lokale Variable eine vorhergehende Zuweisung zu dieser Variablen haben muss, und der Java-Compiler überprüft Schleifen und Verzweigungen, um sicherzustellen, dass die Zuweisung immer vor jeder Verwendung erfolgt (aus diesem Grund ist der gesamten Zuweisung ein ganzer Spezifikationsabschnitt zugeordnet dazu). Die Grundregel lautet:

  • Für jeden Zugriff auf eine lokale Variable oder ein leeres Endfeld muss vor dem Zugriff definitiv zugewiesen werden x, da xsonst ein Fehler bei der Kompilierung auftritt.

Im int d = d + 1; wird der Zugriff auf ddie lokale Variable fine aufgelöst. Da djedoch vor dem dZugriff noch keine Zuweisung erfolgt ist, gibt der Compiler einen Fehler aus. In int c = c = 1, c = 1passiert zuerst, der Abtretungsempfänger c, und dann cauf das Ergebnis dieser Zuordnung initialisiert wird (das ist 1).

Beachten Sie, dass aufgrund bestimmter Zuordnungsregeln, die lokale Variablendeklaration int d = (d = 1) + d; wird erfolgreich kompiliert ( im Gegensatz zu der Felddeklarationint b = (b = 1) + b ), da auf djeden Fall von der Zeit zugewiesen wird die endgültige derreicht ist.

nneonneo
quelle
+1 für die Referenzen, aber ich denke, Sie haben diesen Wortlaut falsch verstanden: "int a = a = 1; kompiliert, weil es gegen (b) verstößt", wenn es gegen eine der 4 Anforderungen verstößt, wird es nicht kompiliert. Es hat jedoch nicht , da es IST auf der linken Seite einer Zuweisung (doppelt negativ in Formulierung von JLS hier nicht viel hilft). In int b = b + 1b ist auf der rechten (nicht auf der linken Seite) der Zuordnung, so dass es diese verletzen würde ...
msam
... Was ich nicht so sicher bin , ist folgende: diese vier Bedingungen , wenn die Erklärung vor der Zuweisung erscheint nicht textlich erfüllt sein müssen, in diesem Fall denke ich , die Erklärung erscheint „textlich“ vor der Zuweisung int x = x = 1, in denen In diesem Fall würde nichts davon zutreffen.
msam
@msam: Es ist ein bisschen verwirrend, aber im Grunde muss man eine der vier Bedingungen verletzen, um eine Vorwärtsreferenz zu machen. Wenn Ihre Vorwärtsreferenz alle vier Bedingungen erfüllt , ist sie illegal.
Nneonneo
@msam: Außerdem wird die vollständige Deklaration erst nach dem Initialisierer wirksam.
Nneonneo
@mrfishie: Große Antwort, aber die Java-Spezifikation enthält überraschend viel Tiefe. Die Frage ist nicht so einfach, wie es an der Oberfläche scheint. (Ich habe einmal eine Untergruppe von Java-Compilern geschrieben, daher bin ich mit vielen Vor- und Nachteilen des JLS ziemlich vertraut.)
Nneonneo
86
int x = x = 1;

ist äquivalent zu

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

während in

int x = x + 1; 

Zuerst müssen wir berechnen, x+1aber der Wert von x ist nicht bekannt, sodass Sie einen Fehler erhalten (der Compiler weiß, dass der Wert von x nicht bekannt ist).

msam
quelle
4
Dies und der Hinweis auf die richtige Assoziativität von OpenSauce fand ich sehr nützlich.
TobiMcNamobi
1
Ich dachte, der Rückgabewert einer Zuweisung sei der zugewiesene Wert, nicht der variable Wert.
zzzzBov
2
@zzzzBov ist korrekt. int x = x = 1;entspricht int x = (x = 1), nicht x = 1; x = x; . Sie sollten dafür keine Compiler-Warnung erhalten.
Nneonneo
int x = x = 1;s entspricht int x = (x = 1)wegen der =
Rechtsassoziativität
1
@nneonneo und int x = (x = 1)ist äquivalent zu int x; x = 1; x = x;(Variablendeklaration, Auswertung des Feldinitialisierers , Zuordnung der Variablen zum Ergebnis dieser Auswertung), daher die Warnung
msam
41

Es ist ungefähr gleichbedeutend mit:

int x;
x = 1;
x = 1;

Erstens int <var> = <expression>;ist immer gleichbedeutend mit

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

In diesem Fall ist Ihr Ausdruck x = 1, was auch eine Aussage ist. x = 1ist eine gültige Anweisung, da die Variable xbereits deklariert wurde. Es ist auch ein Ausdruck mit dem Wert 1, der dann erneut zugewiesen xwird.

OpenSauce
quelle
Ok, aber wenn es so lief, wie Sie sagen, warum gibt die zweite Anweisung im Klassenumfang einen Fehler aus? Ich meine, Sie erhalten einen Standardwert 0für Ints, daher würde ich erwarten, dass das Ergebnis 1 ist, nicht das undefined reference.
Marcin
Schauen Sie sich die Antwort von @izogfif an. Scheint zu funktionieren, da der C ++ - Compiler Variablen Standardwerte zuweist. Genauso wie Java für Variablen auf Klassenebene.
Marcin
@Marcin: In Java werden Ints nicht auf 0 initialisiert, wenn es sich um lokale Variablen handelt. Sie werden nur dann auf 0 initialisiert, wenn es sich um Mitgliedsvariablen handelt. Also in deiner zweiten Zeile x + 1hat kein definierter Wert, weil xnicht initialisiert ist.
OpenSauce
1
@OpenSauce Wird x jedoch als Mitgliedsvariable definiert ("im Klassenbereich").
Jacob Raihle
@ JacobRaihle: Ah ok, habe diesen Teil nicht entdeckt. Ich bin nicht sicher, ob der Bytecode zum Initialisieren einer Variablen auf 0 vom Compiler generiert wird, wenn er sieht, dass es eine explizite Initialisierungsanweisung gibt. Es gibt hier einen Artikel, der einige Details zur Klassen- und Objektinitialisierung enthält, obwohl ich nicht glaube, dass er genau dieses Problem angeht
OpenSauce
12

In Java oder in einer modernen Sprache kommt die Zuordnung von rechts.

Angenommen, Sie haben zwei Variablen x und y,

int z = x = y = 5;

Diese Anweisung ist gültig und so teilt der Compiler sie auf.

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

Aber in deinem Fall

int x = x + 1;

Der Compiler hat eine Ausnahme gemacht, weil er sich so aufteilt.

x = 1; // oops, it isn't declared because assignment comes from the right.
Sri Harsha Chilakapati
quelle
Warnung ist auf x = x nicht x = 1
Asim Ghaffar
8

int x = x = 1; ist ungleich zu:

int x;
x = 1;
x = x;

javap hilft uns wieder, dies sind JVM-Anweisungen, die für diesen Code generiert wurden:

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

eher wie:

int x = 1;
x = 1;

Hier ist kein Grund, einen undefinierten Referenzfehler auszulösen. Es gibt jetzt die Verwendung von Variablen vor ihrer Initialisierung, sodass dieser Code den Spezifikationen vollständig entspricht. Tatsächlich werden Variablen überhaupt nicht verwendet , nur Zuweisungen. Und der JIT-Compiler wird noch weiter gehen und solche Konstruktionen eliminieren. Ehrlich gesagt verstehe ich nicht, wie dieser Code mit der JLS-Spezifikation für die Initialisierung und Verwendung von Variablen verbunden ist. Keine Nutzung keine Probleme. ;)

Bitte korrigieren Sie, wenn ich falsch liege. Ich kann nicht herausfinden, warum andere Antworten, die sich auf viele JLS-Absätze beziehen, so viele Pluspunkte sammeln. Diese Absätze haben mit diesem Fall nichts gemeinsam. Nur zwei Serienzuweisungen und nicht mehr.

Wenn wir schreiben:

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

entspricht:

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

Der Ausdruck ganz rechts wird nur den Variablen einzeln ohne Rekursion zugewiesen. Wir können Variablen nach Belieben durcheinander bringen:

a = b = c = f = e = d = a = a = a = a = a = e = f = 5;
Mikhail
quelle
7

Wenn int x = x + 1;Sie 1 zu x hinzufügen, ist der Wert von noch xnicht erstellt.

Aber in int x=x=1;wird ohne Fehler kompiliert, weil Sie 1 zuweisen x.

Alya'a Gamal
quelle
5

Ihr erster Code enthält einen zweiten =anstelle eines Pluszeichens. Dies wird überall kompiliert, während der zweite Code an keiner Stelle kompiliert wird.

Joe Elleson
quelle
5

Im zweiten Code wird x vor seiner Deklaration verwendet, während es im ersten Code nur zweimal zugewiesen wird, was keinen Sinn ergibt, aber gültig ist.

WilQu
quelle
5

Lassen Sie es uns Schritt für Schritt aufschlüsseln, richtig assoziativ

int x = x = 1

x = 1, ordne einer Variablen x 1 zu

int x = x, weisen Sie sich selbst zu, was x ist, als int. Da x zuvor als 1 zugewiesen wurde, behält es 1 bei, wenn auch redundant.

Das passt gut zusammen.

int x = x + 1

x + 1, füge eins zu einer Variablen x hinzu. Wenn x jedoch undefiniert ist, führt dies zu einem Kompilierungsfehler.

int x = x + 1Daher kompiliert diese Zeile Kompilierungsfehler, da der rechte Teil der Gleichheit nicht kompiliert, indem einer nicht zugewiesenen Variablen eine hinzugefügt wird

steventnorris
quelle
Nein, es ist richtig assoziativ, wenn es zwei =Operatoren gibt, also ist es dasselbe wie int x = (x = 1);.
Jeppe Stig Nielsen
Ah, meine Befehle ab. Das tut mir leid. Hätte sie rückwärts machen sollen. Ich habe es jetzt umgeschaltet.
Steventnorris
3

Die zweite int x=x=1ist kompiliert, weil Sie den Wert dem x zuweisen, aber in einem anderen Fall wird int x=x+1hier die Variable x nicht initialisiert. Denken Sie daran, dass in Java lokale Variablen nicht auf den Standardwert initialisiert werden. Hinweis Wenn es sich auch int x=x+1im Klassenbereich um ( ) handelt, wird auch ein Kompilierungsfehler ausgegeben, da die Variable nicht erstellt wird.

Krushna
quelle
2
int x = x + 1;

Kompiliert erfolgreich in Visual Studio 2008 mit Warnung

warning C4700: uninitialized local variable 'x' used`
izogfif
quelle
2
Interessant. Ist es C / C ++?
Marcin
@Marcin: Ja, es ist C ++. @msam: Entschuldigung, ich glaube ich habe Tag cstatt gesehen, javaaber anscheinend war es die andere Frage.
izogfif
Es wird kompiliert, weil Compiler in C ++ Standardwerte für primitive Typen zuweisen. Verwenden Sie bool y;und y==truewird false zurückgeben.
Sri Harsha Chilakapati
@SriHarshaChilakapati, ist es eine Art Standard im C ++ - Compiler? Denn wenn ich void main() { int x = x + 1; printf("%d ", x); }in Visual Studio 2008 kompiliere , erhalte ich beim Debuggen die Ausnahme Run-Time Check Failure #3 - The variable 'x' is being used without being initialized.und in Release wird die Nummer 1896199921in der Konsole gedruckt.
izogfif
1
@SriHarshaChilakapati Über andere Sprachen sprechen: In C # gelten für ein staticFeld (statische Variable auf Klassenebene) dieselben Regeln. Zum Beispiel ein Feld, public static int x = x + 1;das in Visual C # ohne Warnung als kompiliert deklariert wurde . Möglicherweise das gleiche in Java?
Jeppe Stig Nielsen
2

x ist nicht initialisiert in x = x + 1;

Die Java-Programmiersprache ist statisch typisiert. Dies bedeutet, dass alle Variablen zuerst deklariert werden müssen, bevor sie verwendet werden können.

Siehe primitive Datentypen

Mohan Raj B.
quelle
3
Die Notwendigkeit, Variablen vor der Verwendung ihrer Werte zu initialisieren, hat nichts mit statischer Typisierung zu tun. Statisch typisiert: Sie müssen angeben, welcher Typ eine Variable ist. Vor Gebrauch initialisieren: Es muss nachweislich einen Wert haben, bevor Sie den Wert verwenden können.
Jon Bright
@ JonBright: Die Notwendigkeit, Variablentypen zu deklarieren, hat auch nichts mit statischer Typisierung zu tun. Beispielsweise gibt es statisch typisierte Sprachen mit Typinferenz.
Hammar
@hammar, so wie ich es sehe, können Sie es auf zwei Arten argumentieren: Mit Typinferenz deklarieren Sie implizit den Typ der Variablen auf eine Weise, die das System ableiten kann. Oder Typinferenz ist eine dritte Möglichkeit, bei der Variablen zur Laufzeit nicht dynamisch typisiert werden, sondern sich auf Quellenebene befinden, abhängig von ihrer Verwendung und den so gemachten Inferenzen. In jedem Fall bleibt die Aussage wahr. Aber Sie haben Recht, ich habe nicht an andere Typsysteme gedacht.
Jon Bright
2

Die Codezeile wird nicht mit einer Warnung kompiliert, da der Code tatsächlich funktioniert. Wenn Sie den Code ausführen int x = x = 1, erstellt Java zuerst die xdefinierte Variable . Dann wird der Zuweisungscode ( x = 1) ausgeführt. Da xbereits definiert, hat das System keine Fehlereinstellung xauf 1. Dies gibt den Wert 1 zurück, da dies nun der Wert von ist x. Daher xwird nun endgültig auf 1 gesetzt.
Java führt den Code grundsätzlich so aus, als wäre es das:

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

In Ihrem zweiten Code muss int x = x + 1die + 1Anweisung xjedoch definiert werden, was bis dahin nicht der Fall ist. Da Zuweisungsanweisungen immer bedeuten, dass der Code rechts von =zuerst ausgeführt wird, schlägt der Code fehl, da er nicht xdefiniert ist. Java würde den Code folgendermaßen ausführen:

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

Complier las die Aussagen von rechts nach links und wir wollten das Gegenteil tun. Deshalb hat es zuerst genervt. Machen Sie es sich zur Gewohnheit, Anweisungen (Code) von rechts nach links zu lesen, damit Sie kein solches Problem haben.

Ramiz Uddin
quelle