Ist endgültig schlecht definiert?

186

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 Xin Skala (long) und neu zu definieren X = scale(10) + 3, werden die Abzüge werden X = 0dann X = 3. Dies bedeutet, dass Xvorübergehend auf 0und 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 finalschlecht definiert?


Hier ist der Code, an dem ich interessiert bin. XIhm werden zwei verschiedene Werte zugewiesen: 0und 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 finalTag 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 amit markiert ist final, erhält er die folgende Ausgabe:

a=5
a=5

Das betrifft nicht den Hauptteil meiner Frage: Wie ändert eine finalVariable ihre Variable?

Kleiner Helfer
quelle
17
Diese Art der Referenzierung des XElements ist wie die Bezugnahme auf ein Unterklassenmitglied, bevor der Superklassenkonstruktor fertig ist. Das ist Ihr Problem und nicht die Definition von final.
Daniu
4
Von JLS: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.
Ivan
1
@Ivan, Hier geht es nicht um Konstanten, sondern um Instanzvariablen. Aber können Sie das Kapitel hinzufügen?
AxelH
9
Nur als Hinweis: Tun Sie dies niemals im Produktionscode. Es ist für alle sehr verwirrend, wenn jemand anfängt, Lücken in der JLS auszunutzen.
Zabuzard
13
Zu Ihrer Information, Sie können genau diese Situation auch in C # erstellen. C # verspricht, dass Schleifen in konstanten Deklarationen zur Kompilierungszeit abgefangen werden, macht jedoch keine solchen Versprechungen in Bezug auf schreibgeschützte Deklarationen, und in der Praxis können Sie in Situationen geraten, in denen der anfängliche Nullwert des Felds von einem anderen Feldinitialisierer beobachtet wird. Wenn es dabei weh tut, tu es nicht . Der Compiler wird Sie nicht speichern.
Eric Lippert

Antworten:

217

Ein sehr interessanter Fund. Um es zu verstehen, müssen wir uns mit der Java Language Specification ( JLS ) befassen .

Der Grund ist, dass finalnur 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:

private static Object x;

public static void main(String[] args) {
    System.out.println(x); // Prints 'null'
}

Wir haben dem Standardwert keinen expliziten Wert zugewiesen x, obwohl er darauf verweist null. Vergleichen Sie das mit §4.12.5 :

Anfangswerte von Variablen

Jede Klassenvariablen , Instanzvariablen oder Arraykomponenten mit einem initialisierten Standardwert , wenn er erstellt ( §15.9 , §15.10.2 )

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:

public static void main(String[] args) {
    Object x;
    System.out.println(x);
    // Compile-time error:
    // variable x might not have been initialized
}

Aus demselben JLS-Absatz:

Eine lokale Variable ( §14.4 , §14.14 ) müssen explizit einen Wert gegeben , bevor es verwendet wird, entweder durch Initialisierung ( §14.4 ) oder Zuweisung ( §15.26 ), in einer Weise , die mit den Regeln für die eindeutige Zuordnung überprüft werden kann ( § 16 (definitive Zuordnung) ).


Letzte Variablen

Nun schauen wir uns finalab §4.12.4 an :

endgültige Variablen

Eine Variable kann als endgültig deklariert werden . Eine endgültige Variable darf nur einmal zugewiesen werden . Es ist ein Fehler bei der Kompilierung, wenn eine endgültige Variable zugewiesen wird, es sei denn, sie wird unmittelbar vor der Zuweisung definitiv nicht zugewiesen ( §16 (Definitive Zuweisung) ).


Erläuterung

Kommen wir nun zu Ihrem Beispiel zurück, das leicht modifiziert wurde:

public static void main(String[] args) {
    System.out.println("After: " + X);
}

private static final long X = assign();

private static long assign() {
    // Access the value before first assignment
    System.out.println("Before: " + X);

    return X + 1;
}

Es gibt aus

Before: 0
After: 1

Erinnern Sie sich an das, was wir gelernt haben. Innerhalb der Methode wurde assignder Variablen Xnoch 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 der assignMethode wird der Variablen Xder Wert zugewiesen 1und finalwir können ihn deshalb nicht mehr ändern. Folgendes würde also aufgrund von nicht funktionieren final:

private static long assign() {
    // Assign X
    X = 1;

    // Second assign after method will crash
    return X + 1;
}

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

private static final long X = X + 1;
// Compile-time error:
// self-reference in initializer

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:

Bei einem Verweis mit einfachem Namen auf eine fin einer Klasse oder Schnittstelle deklarierte Klassenvariable Chandelt es sich um einen Fehler zur Kompilierungszeit, wenn :

  • Die Referenz erscheint entweder in einem Klassenvariableninitialisierer von Coder in einem statischen Initialisierer von C( §8.7 ); und

  • Die Referenz erscheint entweder im Initialisierer des feigenen Deklarators oder an einer Stelle links vom fDeklarator. und

  • Die Referenz befindet sich nicht auf der linken Seite eines Zuweisungsausdrucks ( §15.26 ); und

  • Die innerste Klasse oder Schnittstelle, die die Referenz einschließt, ist C.

Es ist einfach, das X = X + 1wird von diesen Regeln abgefangen, der Methodenzugriff nicht. Sie listen sogar dieses Szenario auf und geben ein Beispiel:

Zugriffe nach Methoden werden auf diese Weise nicht überprüft.

class Z {
    static int peek() { return j; }
    static int i = peek();
    static int j = 1;
}
class Test {
    public static void main(String[] args) {
        System.out.println(Z.i);
    }
}

erzeugt die Ausgabe:

0

da die Variable Initialisierer für iVerwendungen der Klassenmethode peek der Wert der Variable zuzugreifen , jbevor jdurch seine variable Initialisierer initialisiert wurde, an welcher Stelle es noch den Standardwert hat ( §4.12.5 ).

Zabuzard
quelle
1
@ Andrew Ja, Klassenvariable, danke. Ja, es würde funktionieren, wenn es keine zusätzlichen Regeln gäbe, die einen solchen Zugriff einschränken: §8.3.3 . Schauen Sie sich die vier Punkte an, die für Klassenvariablen angegeben wurden (der erste Eintrag). Der Methodenansatz im OP-Beispiel wird von diesen Regeln nicht erfasst, daher können wir über Xdie 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.
Zabuzard
4
Das Problem ist, dass Sie Instanzmethoden vom Konstruktor aus aufrufen können, was wahrscheinlich nicht erlaubt sein sollte. Auf der anderen Seite ist es nicht zulässig, Einheimische zuzuweisen, bevor sie super anrufen, was nützlich und sicher wäre. Stelle dir das vor.
Stellen Sie Monica
1
@ Andrew, du bist wahrscheinlich der einzige hier, der tatsächlich erwähnt hat forwards references(der auch Teil des JLS ist). Dies ist so einfach ohne diese lange Antwort stackoverflow.com/a/49371279/1059372
Eugene
1
"Die erste Zuordnung ändert dann die Referenz." In diesem Fall handelt es sich nicht um einen Referenztyp, sondern um einen primitiven Typ.
Fabian
1
Diese Antwort ist richtig, wenn auch etwas lang. :-) Ich denke, das tl; dr ist, dass das OP ein Tutorial zitiert hat , das besagt, dass "[ein letztes] Feld sich nicht ändern kann", nicht das JLS. Die Tutorials von Oracle sind zwar recht gut, decken jedoch nicht alle Randfälle ab. Für die Frage des OP müssen wir zur tatsächlichen JLS-Definition von final gehen - und diese Definition erhebt nicht die Behauptung (dass das OP zu Recht in Frage stellt), dass sich der Wert eines endgültigen Feldes niemals ändern kann.
Yshavit
22

Hier 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, 0wenn Sie ohne Zuweisung darauf zugreifen.

Wenn Sie Xohne vollständige Zuweisung zugreifen , enthält es die Standardwerte von long 0, also die Ergebnisse.

Suresh Atta
quelle
3
Das Schwierige daran ist, dass wenn Sie den Wert nicht zuweisen, er nicht mit dem Standardwert zugewiesen wird, aber wenn Sie ihn verwendet haben, um sich selbst den "endgültigen" Wert zuzuweisen, wird er ...
AxelH
2
@AxelH Ich verstehe, was du damit meinst. Aber so sollte es funktionieren, sonst bricht die Welt zusammen;).
Suresh Atta
20

Kein Fehler.

Wenn der erste Anruf von scaleaufgerufen wird

private static final long X = scale(10);

Es versucht zu bewerten return X * value. Xwurde noch kein Wert zugewiesen und daher wird der Standardwert für a longverwendet (dh 0).

Diese Codezeile ergibt X * 10also , dh 0 * 10welche 0.

OldCurmudgeon
quelle
8
Ich glaube nicht, dass OP das verwirrt. Was verwirrt ist X = scale(10) + 3. Da X, wenn von der Methode verwiesen, ist 0. Aber danach ist es 3. OP glaubt Xalso, dass dem zwei verschiedene Werte zugewiesen sind, die in Konflikt stehen würden final.
Zabuzard
4
@Zabuza erklärt dies nicht mit dem " Es versucht zu bewerten return X * value.Xlong0 Wurde noch kein Wert zugewiesen und nimmt daher den Standardwert für ein was ist . "? Es wird nicht gesagt X, dass der Standardwert zugewiesen ist, sondern dass der Wert durch den Standardwert X"ersetzt" wird (bitte zitieren Sie diesen Begriff nicht;)).
AxelH
14

Es ist überhaupt kein Fehler, einfach gesagt, es ist keine illegale Form von Vorwärtsreferenzen, nichts weiter.

String x = y;
String y = "a"; // this will not compile 


String x = getIt(); // this will compile, but will be null
String y = "a";

public String getIt(){
    return y;
}

Es ist einfach durch die Spezifikation erlaubt.

Um genau zu sein, hier stimmt dies überein:

private static final long X = scale(10) + 3;

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 erhalten X. Auch dies ist in der Spezifikation zulässig (genauer gesagt ist dies nicht verboten), sodass es einwandfrei funktioniert

Eugene
quelle
gute Antwort! Ich bin nur neugierig, warum die Spezifikation das Kompilieren des zweiten Falls ermöglicht. Ist es die einzige Möglichkeit, den "inkonsistenten" Zustand eines endgültigen Feldes zu sehen?
Andrew Tobilko
@ Andrew das hat mich auch ziemlich lange gestört, ich neige dazu zu denken, es ist das C ++ oder C macht es (keine Ahnung, ob dies wahr ist)
Eugene
@ Andrew: Weil etwas anderes zu tun wäre, um den Turing-Unvollständigkeitssatz zu lösen.
Joshua
9
@Joshua: Ich denke, Sie vermischen hier eine Reihe verschiedener Konzepte: (1) das Halteproblem, (2) das Entscheidungsproblem, (3) Godels Unvollständigkeitssatz und (4) Turing-vollständige Programmiersprachen. Compiler-Autoren versuchen nicht, das Problem zu lösen. "Wird diese Variable definitiv zugewiesen, bevor sie verwendet wird?" Perfekt, weil dieses Problem der Lösung des Halteproblems entspricht und wir wissen, dass wir dies nicht tun können.
Eric Lippert
4
@ EricLippert: Haha, hoppla. Unvollständigkeit und Stillstandsprobleme nehmen in meinem Kopf den gleichen Platz ein.
Joshua
4

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:

public class Demo1 {
    private static final long DemoLong1 = 1000;
}

Der generierte Bytecode ähnelt dem folgenden:

public class Demo2 {
    private static final long DemoLong2;

    static {
        DemoLong2 = 1000;
    }
}

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:

public class RecursiveStatic {
    private static final long X;

    private static long scale(long value) {
        return X * value;
    }

    static {
        X = scale(10);
    }

    public static void main(String[] args) {
        System.out.println(scale(5));
    }
}
  1. Die JVM lädt den RecursiveStatic als Einstiegspunkt für das JAR.
  2. Der Klassenladeprogramm führt den statischen Initialisierer aus, wenn die Klassendefinition geladen wird.
  3. Der Initialisierer ruft die Funktion scale(10)zum Zuweisen des static finalFeldes auf X.
  4. Das scale(long) Funktion wird ausgeführt, während die Klasse teilweise initialisiert ist und den nicht initialisierten Wert liest, Xdessen Standardwert long oder 0 ist.
  5. Der Wert von 0 * 10wird zugewiesenX und der Klassenlader wird abgeschlossen.
  6. Die JVM führt den öffentlichen statischen void-Hauptmethodenaufruf aus, scale(5)der 5 mit dem jetzt initialisierten XWert 0 multipliziert und 0 zurückgibt.

Das statische Endfeld Xwird nur einmal zugewiesen, wobei die Garantie des finalSchlü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 + 3welcher Wert ist, 3und die Hauptmethode gibt das Ergebnis aus 3 * 5, dessen Wert der Wert ist 15.

psaxton
quelle
3

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.

Kafein
quelle