Warum löst diese Anweisung keinen StackOverflowError aus?

74

Ich habe gerade diesen seltsamen Code in einer anderen Frage gesehen. Ich dachte, es würde zu einem StackOverflowErrorWerfen führen, aber es tut nicht ...

public class Node {
    private Object one;
    private Object two;
    public static Node NIL = new Node(Node.NIL, Node.NIL);

    public Node(Object one, Object two) {
        this.one = one;
        this.two = two;
    }
}

Ich dachte, es würde explodieren, weil die Node.NILReferenzierung selbst erstellt werden sollte.

Ich kann nicht herausfinden, warum es nicht so ist.

Anthony Raymond
quelle
7
wahrscheinlich wegen, staticaber ich bin nicht sicher
XtremeBaumer
28
Was ich erwarten würde, ist, dass das NILFeld so aufgebaut ist, wie es deklariert wurde new Node(null, null), denn wenn der Konstruktor aufgerufen wird, Node.NILwurde noch nichts festgelegt.
Khelwood
@khelwood yep, basierend auf der Antwort habe ich den gleichen Gedanken verstanden.
Anthony Raymond
4
Bitte verwenden Sie dies niemals im Produktionscode. Es kann eine nette Kleinigkeit sein, aber ich würde dies als absichtliche Verschleierung betrachten.
Chi
@chi Ich bin mir nicht sicher, aber ich habe diesen Code bei einer anderen Frage gesehen und war verwirrt darüber, wie er funktionieren könnte.
Anthony Raymond

Antworten:

100

NIList eine statische Variable. Es wird einmal initialisiert, wenn die Klasse initialisiert wird. Bei der Initialisierung wird eine einzelne NodeInstanz erstellt. Die Erstellung davon Nodelöst nicht die Erstellung anderer NodeInstanzen aus, sodass es keine unendliche Kette von Aufrufen gibt. Das Übergeben Node.NILan den Konstruktoraufruf hat den gleichen Effekt wie das Übergeben null, da Node.NILes beim Aufruf des Konstruktors noch nicht initialisiert ist. Daher public static Node NIL = new Node(Node.NIL, Node.NIL);ist das gleiche wie public static Node NIL = new Node(null, null);.

Wenn es sich andererseits NILum eine Instanzvariable handelt (und nicht als Argument an den NodeKonstruktor übergeben wurde, da der Compiler Sie in diesem Fall daran gehindert hätte, sie an den Konstruktor zu übergeben), wird sie jedes Mal initialisiert, wenn eine Instanz erstellt wird of Nodewurde erstellt, wodurch eine neue NodeInstanz erstellt wurde, deren Erstellung eine andere NILInstanzvariable initialisierte , was zu einer unendlichen Kette von Konstruktoraufrufen führte, die enden würden StackOverflowError.

Eran
quelle
Danke, du hast es klarer gemacht, die Art und Weise, wie es funktioniert, ist für mich immer noch seltsam. Aber zumindest verstehe ich, warum es so funktioniert.
Anthony Raymond
5
Wenn NILes sich um eine Instanzvariable handelt, wird sie nicht mit dem Fehler kompiliert Cannot reference a field before it is defined.
Florian Genser
@FlorianGenser Guter Punkt. Ich habe diesen Teil geschrieben, bevor ich bemerkt habe, dass er Node.NILan den Konstruktor übergeben wurde.
Eran
3
java.awt.Color ist eigentlich eine gute Demonstration statischer Variablen in Aktion. Es hat eine Reihe verschiedener Farben, wie Color.BLUE, das auch Verweise auf alle anderen Farben enthält ... Das hat mich damals geblendet, als ich zum ersten Mal in Java angefangen habe.
sfdcfox
Danke für das @sfdcfox, ich werde mir das ansehen.
Anthony Raymond
27

Die Variable NIL erhält zuerst den Wert nullund wird dann von oben nach unten initialisiert. Es ist keine Funktion und nicht rekursiv definiert. Jedes statische Feld, das Sie vor der Initialisierung verwenden, hat den Standardwert und Ihr Code ist der gleiche wie

public static Node {
    public static Node NIL;

    static {
        NIL = new Node(null /*Node.NIL*/, null /*Node.NIL*/);
    }

    public Node(Object one, Object two) {
        // Assign values to fields
    }
}

Dies unterscheidet sich nicht vom Schreiben

NIL = null; // set implicitly
NIL = new Node(NIL, NIL);

Wenn Sie eine Funktion oder Methode wie diese definieren, erhalten Sie eine StackoverflowException

Node NIL(Node a, Node b) {
    return NIL(NIL(a, b), NIL(a, b));
}
Peter Lawrey
quelle
20

Der Schlüssel zum Verständnis, warum es keine unendliche NodeInitialisierung verursacht, besteht darin, dass die JVM bei der Initialisierung der Klasse den Überblick behält und eine Neuinitialisierung während eines rekursiven Verweises auf die Klasse innerhalb ihrer ursprünglichen Initialisierung vermeidet . Dies wird in diesem Abschnitt der Sprachspezifikation beschrieben :

Da die Java-Programmiersprache Multithreading ist, erfordert die Initialisierung einer Klasse oder Schnittstelle eine sorgfältige Synchronisierung, da ein anderer Thread möglicherweise versucht, dieselbe Klasse oder Schnittstelle gleichzeitig zu initialisieren. Es besteht auch die Möglichkeit, dass die Initialisierung einer Klasse oder Schnittstelle im Rahmen der Initialisierung dieser Klasse oder Schnittstelle rekursiv angefordert wird . Beispielsweise kann ein Variableninitialisierer in Klasse A eine Methode einer nicht verwandten Klasse B aufrufen, die wiederum eine Methode der Klasse A aufruft. Die Implementierung der Java Virtual Machine ist für die Synchronisierung und rekursive Initialisierung mithilfe von verantwortlich folgendes Verfahren.

Während der statische Initialisierer die statische Instanz erstellt NIL, führt der Verweis auf Node.NILals Teil des Konstruktoraufrufs den statischen Initialisierer nicht erneut aus. Stattdessen verweist es nur auf den Wert, den die Referenz NILzu diesem Zeitpunkt hat, nullin diesem Fall.

M Anouti
quelle