Ist final schlecht definiert?

Lesezeit: 11 Minuten

Benutzer-Avatar
Kleiner Helfer

Zuerst ein Rätsel: Was gibt der folgende Code aus?

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 im Maßstab (lang) und neu definieren X = scale(10) + 3werden die Drucke sein X = 0 dann X = 3. Das bedeutet, dass X vorübergehend eingestellt ist 0 und später eingestellt 3. Dies ist ein Verstoß gegen final!

Der statische Modifikator wird in Kombination mit dem finalen Modifikator auch verwendet, um Konstanten zu definieren. Der letzte Modifikator gibt an, dass der Wert von Dieses Feld kann nicht geändert werden.

Quelle: https://docs.oracle.com/javase/tutorial/java/javaOO/classvars.html [emphasis added]


Meine Frage: Ist das ein Bug? Ist final schlecht definiert?


Hier ist der Code, der mich interessiert.
X werden zwei unterschiedliche Werte zugewiesen: 0 und 3. Ich halte dies für einen Verstoß gegen 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 Java Static Final Field Initialization Order gekennzeichnet. Ich glaube, dass diese Frage ist nicht ein Duplikat, da die andere Frage die Reihenfolge der Initialisierung anspricht, während meine Frage eine zyklische Initialisierung in Kombination mit der adressiert final Schild. Allein aus der anderen Frage würde ich nicht verstehen können, warum der Code in meiner Frage keinen Fehler macht.

Dies wird besonders deutlich, wenn man sich die Ausgabe ansieht, die ernesto erhält: wann a ist markiert mit finalerhält er folgende Ausgabe:

a=5
a=5

was nicht den Hauptteil meiner Frage betrifft: Wie funktioniert a final Variable ihre Variable ändern?

  • Diese Art der Referenzierung der X member ist wie der Verweis auf ein Unterklassenmitglied, bevor der Superklassenkonstruktor fertig ist, das ist Ihr Problem und nicht die Definition von final.

    – daniu

    19. März 2018 um 15:00 Uhr

  • 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.

    – Iwan

    19. März 2018 um 15:17 Uhr

  • @Ivan, hier geht es nicht um Konstanten, sondern um Instanzvariablen. Aber kannst du das Kapitel hinzufügen?

    – AxelH

    19. März 2018 um 15:19 Uhr

  • Nur als Hinweis: Tun Sie dies niemals im Produktionscode. Es ist für alle super verwirrend, wenn jemand anfängt, Schlupflöcher im JLS auszunutzen.

    – Zabuzard

    19. März 2018 um 16:43 Uhr

  • Zu Ihrer Information, Sie können genau dieselbe Situation auch in C# erstellen. C# verspricht eine Schleife Konstante Deklarationen werden zur Kompilierzeit abgefangen, machen aber keine solchen Versprechungen schreibgeschützt 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, mach es nicht. Der Compiler wird Sie nicht retten.

    – Eric Lippert

    19. März 2018 um 20:59 Uhr

Benutzer-Avatar
Zabuzard

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

Der Grund ist, dass final erlaubt nur einen Abtretung. Der Standardwert ist jedoch nein Abtretung. Eigentlich alle solche Variable (Klassenvariable, Instanzvariable, Array-Komponente) zeigt auf seine Standardwert von Anfang an, vorher Zuordnungen. Die erste Zuweisung ä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 nicht explizit einen Wert zugewiesen xobwohl es darauf hindeutet null, es ist der Standardwert. Vergleichen Sie das damit §4.12.5:

Anfangswerte von Variablen

Jeder KlassenvariableInstanzvariable oder Array-Komponente wird mit a initialisiert Standardwert wann ist es erstellt (§15.9, §15.10.2)

Beachten Sie, dass dies nur für diese Art von Variablen gilt, wie in unserem Beispiel. Dies 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:

EIN lokale Variable (§14.4, §14.14) muss sein explizit einen Wert gegeben bevor es verwendet wird, entweder durch Initialisierung (§14.4) oder Zuordnung (§15.26), die anhand der Regeln zur eindeutigen Zuordnung (§16 (Konkreter Auftrag)).


Letzte Variablen

Jetzt werfen wir einen Blick auf finalaus §4.12.4:

Finale Variablen

Eine Variable kann deklariert werden Finale. EIN Finale Variable darf nur sein einmal zugeordnet. Es ist ein Kompilierungsfehler, wenn a Finale Variable zugewiesen wird, es sei denn, dies ist der Fall unmittelbar vor der Zuordnung definitiv nicht zugeordnet (§16 (Konkreter Auftrag)).


Erläuterung

Nun zurück zu Ihrem Beispiel, leicht modifiziert:

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 assign Die Variable X war Nicht zugeordnet noch ein Wert. Daher zeigt es auf seinen Standardwert, da es ein ist Klassenvariable und laut JLS zeigen diese Variablen immer sofort auf ihre Standardwerte (im Gegensatz zu lokalen Variablen). Nach dem assign Methode die Variable X wird der Wert zugewiesen 1 und wegen final wir können es 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 im JLS

Dank @Andrew habe ich einen JLS-Absatz gefunden, der genau dieses Szenario abdeckt, er demonstriert es auch.

Aber zuerst werfen wir einen Blick auf

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

Warum ist dies nicht erlaubt, der Zugriff aus der Methode hingegen schon? Schauen Sie sich an §8.3.3 was darüber spricht, wann der Zugriff auf Felder eingeschränkt ist, wenn das Feld noch nicht initialisiert wurde.

Es listet einige Regeln auf, die für Klassenvariablen relevant sind:

Für eine Referenz mit einfachem Namen auf eine Klassenvariable f in Klasse oder Schnittstelle deklariert Ces ist ein Kompilierungsfehler if:

  • Die Referenz erscheint entweder in einem Klassenvariablen-Initialisierer von C oder in einem statischen Initialisierer von C (§8.7); und

  • Die Referenz erscheint entweder im Initialisierer von f‘s eigener Deklarator oder an einem Punkt links davon fder Deklarator von ; und

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

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

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

Zugriffe über Methoden werden auf diese Weise nicht geprüft, also:

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

weil der Variableninitialisierer für i verwendet die Klassenmethode peek, um auf den Wert der Variablen zuzugreifen j Vor j wurde durch seinen Variableninitialisierer initialisiert, an welchem ​​Punkt es hat immer noch seinen Standardwert (§4.12.5).

  • @Andrew Ja, Klassenvariable, danke. Ja, es möchten funktionieren, wenn es nicht einige zusätzliche Regeln gäbe, die diesen Zugriff einschränken: §8.3.3. Schauen Sie sich die vier angegebenen Punkte an Klassenvariablen (der erste Eintrag). Der methodische Ansatz in OPs Beispiel wird von diesen Regeln nicht erfasst, daher können wir darauf zugreifen X aus der Methode. Ich hätte nicht so viel dagegen. Es hängt nur davon ab, wie genau das JLS die Dinge definiert, die im Detail funktionieren sollen. Ich würde niemals solchen Code verwenden, es nutzt nur einige Regeln im JLS aus.

    – Zabuzard

    19. März 2018 um 16:19 Uhr


  • Das Problem ist, dass Sie Instanzmethoden vom Konstruktor aus aufrufen können, was wahrscheinlich nicht hätte erlaubt sein sollen. Andererseits ist die Zuweisung von Einheimischen vor dem Aufrufen von Super, was nützlich und sicher wäre, nicht zulässig. Stelle dir das vor.

    – Monika wieder einsetzen

    19. März 2018 um 16:58 Uhr


  • @Andrew du bist wahrscheinlich der einzige hier, der das tut eigentlich genannt forwards references (die auch Teil der JLS sind). das ist so einfach ohne diese laaange Antwort stackoverflow.com/a/49371279/1059372

    – Eugen

    19. März 2018 um 19:58 Uhr

  • “Die erste Zuweisung ändert dann den Bezug.” In diesem Fall handelt es sich nicht um einen Referenztyp, sondern um einen primitiven Typ.

    – Fabian

    20. März 2018 um 11:53 Uhr


  • Diese Antwort ist richtig, wenn auch etwas lang. 🙂 Ich denke, das tl; dr ist, dass das OP a zitiert Lernprogramm das sagte, dass “[a final] Das Feld kann sich nicht ändern”, nicht das JLS. Obwohl die Tutorials von Oracle ziemlich gut sind, decken sie nicht alle Grenzfälle ab. Für die Frage des OP müssen wir zur eigentlichen JLS-Definition von final gehen – und diese Definition macht es nicht die Behauptung (die das OP zu Recht in Frage stellt), dass sich der Wert eines endgültigen Felds niemals ändern kann.

    – yshavit

    22. März 2018 um 9:02 Uhr


Benutzer-Avatar
Sicher Atta

Das hat hier nichts mit Finale zu tun.

Da es sich auf Instanz- oder Klassenebene befindet, enthält es den Standardwert, wenn noch nichts zugewiesen wird. Das ist der Grund, warum Sie sehen 0 wenn Sie ohne Zuweisung darauf zugreifen.

Wenn Sie zugreifen X Ohne vollständige Zuweisung enthält es die Standardwerte von long which is 0daher die Ergebnisse.

  • 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

    19. März 2018 um 15:06 Uhr

  • @AxelH Ich verstehe, was du damit meinst. Aber so sollte es funktionieren, sonst bricht die Welt zusammen ;).

    – Suresh Atta

    19. März 2018 um 16:48 Uhr


Benutzer-Avatar
Alter Geizhals

Kein Fehler.

Beim ersten Anruf an scale wird von angerufen

private static final long X = scale(10);

Es versucht zu bewerten return X * value. X wurde noch kein Wert zugewiesen und ist daher der Standardwert für a long verwendet wird (also 0).

Diese Codezeile wird also ausgewertet X * 10 dh 0 * 10 welches ist 0.

  • Ich glaube nicht, dass das das ist, was OP verwirrt. Was verwirrt ist X = scale(10) + 3. Seit Xwenn von der Methode darauf verwiesen wird, ist 0. Aber danach ist es so 3. Also denkt OP das X zwei unterschiedliche Werte zugewiesen, die in Konflikt geraten würden final.

    – Zabuzard

    19. März 2018 um 15:07 Uhr

  • @Zabuza ist das nicht mit dem “Es versucht zu bewerten return X * value. X wurde noch kein Wert zugewiesen und nimmt daher den Standardwert für a an long welches ist 0.„? Es wird nicht gesagt X mit dem Standardwert belegt ist, aber dass die X wird durch den Standardwert “ersetzt” (bitte diesen Begriff nicht zitieren 😉 ).

    – AxelH

    19. März 2018 um 15:08 Uhr


Benutzer-Avatar
Eugen

Es ist überhaupt kein Fehler, einfach gesagt, es ist kein illegale Form von Vorwärtsverweisen, mehr nicht.

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 Ihr Beispiel zu nehmen, das ist genau der Punkt, an dem dies zutrifft:

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

Du machst ein Vorwärtsreferenz zu scale Das ist wie gesagt in keiner Weise illegal, erlaubt Ihnen aber, den Standardwert von zu erhalten X. Auch dies ist von der Spezifikation erlaubt (genauer gesagt nicht verboten), also funktioniert es einwandfrei

Benutzer-Avatar
Psaxton

Member auf Klassenebene können im Code innerhalb der Klassendefinition initialisiert werden. Der kompilierte Bytecode kann die Klassenmitglieder nicht inline initialisieren. (Instanzmitglieder werden ähnlich behandelt, aber dies ist für die bereitgestellte Frage nicht relevant.)

Wenn man so etwas schreibt:

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

Der generierte Bytecode würde dem Folgenden ähneln:

public class Demo2 {
    private static final long DemoLong2;

    static {
        DemoLong2 = 1000;
    }
}

Der Initialisierungscode wird in einen statischen Initialisierer platziert, der ausgeführt wird, wenn der Klassenlader die Klasse zum ersten Mal lädt. Mit diesem Wissen würde Ihre ursprüngliche Probe der folgenden ähneln:

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 die RecursiveStatic als Einstiegspunkt der JAR-Datei.
  2. Der Klassenlader führt den statischen Initialisierer aus, wenn die Klassendefinition geladen wird.
  3. Der Initialisierer ruft die Funktion auf scale(10) die zuzuweisen static final aufstellen X.
  4. Das scale(long) Die Funktion wird ausgeführt, während die Klasse teilweise initialisiert ist und den nicht initialisierten Wert von liest X was der Standardwert von long oder 0 ist.
  5. Der Wert von 0 * 10 zugeordnet ist X und der Klassenlader wird abgeschlossen.
  6. Die JVM führt den Aufruf der öffentlichen statischen Hauptmethode void aus scale(5) was 5 mit dem jetzt initialisierten multipliziert X Wert von 0 gibt 0 zurück.

Das statische letzte Feld X wird unter Wahrung der Garantie nur einmal abgetreten final Stichwort. Für die nachfolgende Abfrage zum Hinzufügen von 3 in der Zuordnung wird der obige Schritt 5 zur Bewertung von 0 * 10 + 3 das ist der Wert 3 und die Hauptmethode druckt das Ergebnis von 3 * 5 das ist der Wert 15.

Benutzer-Avatar
Kafein

Das Lesen eines nicht initialisierten Felds eines Objekts sollte zu einem Kompilierungsfehler führen. Unglücklicherweise für Java ist dies nicht der Fall.

Ich denke, der grundlegende Grund, warum dies der Fall ist, liegt tief in der Definition, wie Objekte instanziiert und konstruiert werden, “versteckt”, obwohl ich die Details des Standards nicht kenne.

In gewisser Weise ist final schlecht definiert, weil es aufgrund dieses Problems nicht einmal seinen erklärten Zweck erfüllt. Wenn jedoch alle Ihre Klassen richtig geschrieben sind, haben Sie dieses Problem nicht. Das bedeutet, dass alle Felder immer in allen Konstruktoren festgelegt sind und kein Objekt jemals erstellt wird, ohne einen seiner Konstruktoren aufzurufen. Das scheint natürlich, bis Sie eine Serialisierungsbibliothek verwenden müssen.

1351050cookie-checkIst final schlecht definiert?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy