Почему этот оператор не бросает StackOverflowError?

Я только что увидел этот странный фрагмент кода в другом вопросе. Я думал, что это приведет к выбросу StackOverflowError, но это не...

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

Я думал, что он взорвется, из-за Node.NIL, ссылающегося на себя для сборки.

Я не могу понять, почему это не так.

Ответ 1

NIL - статическая переменная. Он инициализируется один раз, когда класс инициализируется. Когда он инициализируется, создается один экземпляр Node. Создание этого Node не вызывает создание каких-либо других экземпляров Node, поэтому нет бесконечной цепочки вызовов. Передача Node.NIL в вызов конструктора имеет тот же эффект, что и передача null, так как Node.NIL еще не инициализируется при вызове конструктора. Поэтому public static Node NIL = new Node(Node.NIL, Node.NIL); совпадает с public static Node NIL = new Node(null, null);.

Если, с другой стороны, NIL была переменной экземпляра (и не передавалась как аргумент конструктору Node, так как компилятор помешал бы вам передать его в конструктор в этом случае), он будет инициализирован каждый раз, когда будет создан экземпляр Node, который создал бы новый экземпляр Node, создание которого инициализировало бы другую переменную экземпляра NIL, что привело бы к бесконечной цепочке вызовов конструктора, которая закончилась бы в StackOverflowError.

Ответ 2

В переменной NIL сначала присваивается значение null, а затем инициализируется один раз сверху вниз. Это не функция и не определяется рекурсивно. Любое статическое поле, которое вы используете до его инициализации, имеет значение по умолчанию, а ваш код такой же, как

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

Это ничем не отличается от написания

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

Если вы определили функцию или метод, как это, вы получите исключение StackoverflowException

Node NIL(Node a, Node b) {
    return NIL(NIL(a, b), NIL(a, b));
}

Ответ 3

Ключ, чтобы понять, почему он не вызывает бесконечную инициализацию, заключается в том, что при инициализации класса Node JVM отслеживает его и избегает повторной инициализации во время рекурсивной ссылки на класс в своей первоначальной инициализации. Это подробно описано в в этом разделе спецификации языка:

Поскольку язык программирования Java является многопоточным, инициализация класса или интерфейса требует тщательной синхронизации, поскольку некоторые другие потоки могут пытаться инициализировать один и тот же класс или интерфейс в одно и то же время. Существует также возможность того, что инициализация класса или интерфейса может быть запрошена рекурсивно как часть инициализации этого класса или интерфейса; например, переменный инициализатор в классе A может вызывать метод несвязанного класса B, который, в свою очередь, может вызвать метод класса A. Реализация виртуальной машины Java отвечает за синхронизацию и рекурсивную инициализацию с помощью следуя процедуре.

Поэтому, когда статический инициализатор создает статический экземпляр NIL, ссылка на Node.NIL как часть вызова конструктора не повторяет повторный запуск статического инициализатора. Вместо этого он просто ссылается на любое значение, которое имеет значение reference NIL, которое в этом случае null.