Zunächst ein Rätsel: Was druckt der folgende Code?
public class RecursiveStatic {
public static void main(String[] args) {
System.out.println(scale(5));
}
private static final long X = scale(10);
private static long scale(long value) {
return X * value;
}
}
Antworten:
0
Spoiler unten.
Wenn Sie drucken X
in Skala (long) und neu zu definieren X = scale(10) + 3
, werden die Abzüge werden X = 0
dann X = 3
. Dies bedeutet, dass X
vorübergehend auf 0
und später auf gesetzt wird 3
. Dies ist eine Verletzung von final
!
Der statische Modifikator wird in Kombination mit dem endgültigen Modifikator auch zum Definieren von Konstanten verwendet. Der letzte Modifikator gibt an, dass sich der Wert dieses Feldes nicht ändern kann .
Quelle: https://docs.oracle.com/javase/tutorial/java/javaOO/classvars.html [Hervorhebung hinzugefügt]
Meine Frage: Ist das ein Fehler? Ist final
schlecht definiert?
Hier ist der Code, an dem ich interessiert bin.
X
Ihm werden zwei verschiedene Werte zugewiesen: 0
und 3
. Ich glaube, dass dies eine Verletzung von ist final
.
public class RecursiveStatic {
public static void main(String[] args) {
System.out.println(scale(5));
}
private static final long X = scale(10) + 3;
private static long scale(long value) {
System.out.println("X = " + X);
return X * value;
}
}
Diese Frage wurde als mögliches Duplikat der statischen endgültigen Feldinitialisierungsreihenfolge von Java gekennzeichnet . Ich glaube, dass diese Frage kein Duplikat ist, da die andere Frage die Reihenfolge der Initialisierung behandelt, während meine Frage eine zyklische Initialisierung in Kombination mit dem final
Tag behandelt. Anhand der anderen Frage allein kann ich nicht verstehen, warum der Code in meiner Frage keinen Fehler macht.
Dies wird besonders deutlich, wenn man sich die Ausgabe ansieht, die ernesto erhält: Wenn er a
mit markiert ist final
, erhält er die folgende Ausgabe:
a=5
a=5
Das betrifft nicht den Hauptteil meiner Frage: Wie ändert eine final
Variable ihre Variable?
quelle
X
Elements ist wie die Bezugnahme auf ein Unterklassenmitglied, bevor der Superklassenkonstruktor fertig ist. Das ist Ihr Problem und nicht die Definition vonfinal
.A blank final instance variable must be definitely assigned (§16.9) at the end of every constructor (§8.8) of the class in which it is declared; otherwise a compile-time error occurs.
Antworten:
Ein sehr interessanter Fund. Um es zu verstehen, müssen wir uns mit der Java Language Specification ( JLS ) befassen .
Der Grund ist, dass
final
nur eine Zuordnung erlaubt . Der Standardwert ist jedoch keine Zuordnung . Tatsächlich zeigt jede solche Variable (Klassenvariable, Instanzvariable, Array-Komponente) vor den Zuweisungen von Anfang an auf ihren Standardwert . Die erste Zuordnung ändert dann die Referenz.Klassenvariablen und Standardwert
Schauen Sie sich das folgende Beispiel an:
Wir haben dem Standardwert keinen expliziten Wert zugewiesen
x
, obwohl er darauf verweistnull
. Vergleichen Sie das mit §4.12.5 :Beachten Sie, dass dies nur für diese Art von Variablen gilt, wie in unserem Beispiel. Es gilt nicht für lokale Variablen, siehe folgendes Beispiel:
Aus demselben JLS-Absatz:
Letzte Variablen
Nun schauen wir uns
final
ab §4.12.4 an :Erläuterung
Kommen wir nun zu Ihrem Beispiel zurück, das leicht modifiziert wurde:
Es gibt aus
Erinnern Sie sich an das, was wir gelernt haben. Innerhalb der Methode wurde
assign
der VariablenX
noch kein Wert zugewiesen . Daher zeigt es auf seinen Standardwert, da es sich um eine Klassenvariable handelt, und laut JLS zeigen diese Variablen immer sofort auf ihre Standardwerte (im Gegensatz zu lokalen Variablen). Nach derassign
Methode wird der VariablenX
der Wert zugewiesen1
undfinal
wir können ihn deshalb nicht mehr ändern. Folgendes würde also aufgrund von nicht funktionierenfinal
:Beispiel in der JLS
Dank @Andrew habe ich einen JLS-Absatz gefunden, der genau dieses Szenario abdeckt und es auch demonstriert.
Aber zuerst schauen wir uns das an
Warum ist dies nicht erlaubt, während der Zugriff von der Methode ist? Schauen Sie sich §8.3.3 an, in dem erläutert wird , wann der Zugriff auf Felder eingeschränkt ist, wenn das Feld noch nicht initialisiert wurde.
Es werden einige Regeln aufgelistet, die für Klassenvariablen relevant sind:
Es ist einfach, das
X = X + 1
wird von diesen Regeln abgefangen, der Methodenzugriff nicht. Sie listen sogar dieses Szenario auf und geben ein Beispiel:quelle
X
die Methode darauf zugreifen . Es würde mir nicht so viel ausmachen. Es kommt nur darauf an, wie genau das JLS die Dinge definiert, die im Detail funktionieren sollen. Ich würde niemals solchen Code verwenden, er nutzt nur einige Regeln im JLS aus.forwards references
(der auch Teil des JLS ist). Dies ist so einfach ohne diese lange Antwort stackoverflow.com/a/49371279/1059372Hier gibt es nichts mit Finale zu tun.
Da es sich auf Instanz- oder Klassenebene befindet, enthält es den Standardwert, wenn noch nichts zugewiesen wurde. Dies ist der Grund, den Sie sehen,
0
wenn Sie ohne Zuweisung darauf zugreifen.Wenn Sie
X
ohne vollständige Zuweisung zugreifen , enthält es die Standardwerte von long0
, also die Ergebnisse.quelle
Kein Fehler.
Wenn der erste Anruf von
scale
aufgerufen wirdEs versucht zu bewerten
return X * value
.X
wurde noch kein Wert zugewiesen und daher wird der Standardwert für along
verwendet (dh0
).Diese Codezeile ergibt
X * 10
also , dh0 * 10
welche0
.quelle
X = scale(10) + 3
. DaX
, wenn von der Methode verwiesen, ist0
. Aber danach ist es3
. OP glaubtX
also, dass dem zwei verschiedene Werte zugewiesen sind, die in Konflikt stehen würdenfinal
.return X * value
.X
long
0
Wurde noch kein Wert zugewiesen und nimmt daher den Standardwert für ein was ist . "? Es wird nicht gesagtX
, dass der Standardwert zugewiesen ist, sondern dass der Wert durch den StandardwertX
"ersetzt" wird (bitte zitieren Sie diesen Begriff nicht;)).Es ist überhaupt kein Fehler, einfach gesagt, es ist keine illegale Form von Vorwärtsreferenzen, nichts weiter.
Es ist einfach durch die Spezifikation erlaubt.
Um genau zu sein, hier stimmt dies überein:
Sie machen einen Vorwärtsverweis darauf
scale
, der in keiner Weise illegal ist, wie zuvor gesagt, aber es ermöglicht Ihnen, den Standardwert von zu erhaltenX
. Auch dies ist in der Spezifikation zulässig (genauer gesagt ist dies nicht verboten), sodass es einwandfrei funktioniertquelle
Mitglieder auf Klassenebene können innerhalb der Klassendefinition im Code initialisiert werden. Der kompilierte Bytecode kann die Klassenmitglieder nicht inline initialisieren. (Instanzmitglieder werden ähnlich behandelt, dies ist jedoch für die gestellte Frage nicht relevant.)
Wenn man so etwas schreibt:
Der generierte Bytecode ähnelt dem folgenden:
Der Initialisierungscode befindet sich in einem statischen Initialisierer, der ausgeführt wird, wenn der Klassenlader die Klasse zum ersten Mal lädt. Mit diesem Wissen würde Ihr Originalmuster dem folgenden ähnlich sein:
scale(10)
zum Zuweisen desstatic final
Feldes aufX
.scale(long)
Funktion wird ausgeführt, während die Klasse teilweise initialisiert ist und den nicht initialisierten Wert liest,X
dessen Standardwert long oder 0 ist.0 * 10
wird zugewiesenX
und der Klassenlader wird abgeschlossen.scale(5)
der 5 mit dem jetzt initialisiertenX
Wert 0 multipliziert und 0 zurückgibt.Das statische Endfeld
X
wird nur einmal zugewiesen, wobei die Garantie desfinal
Schlüsselworts erhalten bleibt . Für die nachfolgende Abfrage des Hinzufügens von 3 in der Zuweisung wird Schritt 5 oben zur Bewertung dessen,0 * 10 + 3
welcher Wert ist,3
und die Hauptmethode gibt das Ergebnis aus3 * 5
, dessen Wert der Wert ist15
.quelle
Das Lesen eines nicht initialisierten Feldes eines Objekts sollte zu einem Kompilierungsfehler führen. Leider für Java nicht.
Ich denke, der grundlegende Grund, warum dies der Fall ist, liegt tief in der Definition, wie Objekte instanziiert und konstruiert werden, "verborgen", obwohl ich die Details des Standards nicht kenne.
In gewissem Sinne ist final schlecht definiert, weil es aufgrund dieses Problems nicht einmal das erreicht, was sein erklärter Zweck ist. Wenn jedoch alle Ihre Klassen richtig geschrieben sind, haben Sie dieses Problem nicht. Das heißt, alle Felder werden immer in allen Konstruktoren festgelegt, und es wird niemals ein Objekt erstellt, ohne einen seiner Konstruktoren aufzurufen. Das scheint natürlich, bis Sie eine Serialisierungsbibliothek verwenden müssen.
quelle