Почему поля не инициализируются перед их возможным первым использованием?

Я был очень сбит с толку, когда обнаружил, что инициализация полей в Java имеет какой-то странный порядок. Пример кода, когда результат init() перезаписывается инициализацией поля:

public abstract class Parent {

    public String parentField = "dupa";

    public Parent(){
        init(); // uhh, bad practice to call abstract method in a super constructor
    }

    protected abstract void init();
}

public class Child extends Parent {

    public String childField = null; // assigning null is unnecessary, another bad practice

    @Override
    protected void init(){
        childField = "initialized";
        System.out.println("After init(): " + childField);
    }
}

...

Child child = new Child(); // OUTPUT: After init(): initialized
System.out.println("After all: " + child.childField); // OUTPUT: After all: null

Я узнал, каков порядок выполнения при вызове new Child();:

  1. Инициализация родительских полей, но childField уже существует и имеет значение по умолчанию (childField = null)
  2. Parent constructor
    • overridden init() called by parent constructor (childField = "initialized")
  3. Инициализация дочерних полей: childField = null (снова)
  4. Детский конструктор

Я знаю, что этот пример полон плохих практик. Однако интуитивный порядок для меня был бы таким: инициализация полей (от родителей к дочерним), затем конструкторы (от родителей к дочерним).

Какова цель такого порядка инициализации? Почему инициализаторы полей не выполняются до их потенциального первого использования? Если поле еще не инициализировано, то почему его разрешено использовать?


person igrek51    schedule 28.01.2017    source источник
comment
Как произойдет инициализация childField в Child до завершения ctor для Parent?   -  person Boris the Spider    schedule 28.01.2017
comment
Причина, по которой вызов переопределяемых методов в конструкторе является плохой практикой, заключается в том, что незавершенный объект передается другому коду. Аналогично, инициализаторы дочерних полей ожидают, что super будет полностью сконструировано.   -  person 4castle    schedule 28.01.2017
comment
Если вы используете конструкторы с параметрами как в дочернем, так и в родительском, вы заметите, что дочерний конструктор вынужден вызывать родительский перед любым своим собственным кодом конструктора. Мне кажется такие вопросы генерируются ботом   -  person efekctive    schedule 28.01.2017
comment
На мой вопрос до сих пор нет ответа: почему разрешено использовать поля до инициализации, если это так опасно? Если вызов абстрактных методов в конструкторе является плохой практикой, почему бы не запретить это компилятором (аналогично любой код перед вызовом super() в конструкторе запрещен)?   -  person igrek51    schedule 29.01.2017
comment
@igrek51, потому что с большой силой приходит большая ответственность (цитируя известное литературное произведение). Например, ножи невероятно полезны, но ничто не мешает вам отрубить себе палец — ничто, кроме обучения. То же самое с Java — вы не можете ожидать, что компилятор защитит программиста от глупых поступков.   -  person Boris the Spider    schedule 29.01.2017


Ответы (1)


Позвольте мне перефразировать ваш «интуитивный» порядок построения нового объекта:

  1. Родительские поля инициализированы
  2. Дочерние поля инициализируются
  3. Родительский конструктор называется
  4. Детский конструктор называется

Ну, это не совсем разумно, потому что инициализация дочерних полей может зависеть от родителя.

Объект можно считать «правильно инициализированным», когда возвращается его конструктор. Соглашаться?

Вместо использования Parent и Child давайте использовать Box и TreasureBox. Чтобы построить TreasureBox, вы начинаете с создания Box. После того, как коробка создана, вы добавляете к ней различные украшения, чтобы она выглядела модно и круто, и вы также можете добавить замок или что-то еще.

Видеть? порядок здесь? Наиболее разумно сначала правильно инициализировать родительский класс перед инициализацией дочернего, что означает, что любая инициализация дочернего класса должна происходить после возврата родительского конструктора. Это именно то, что делает Java.

Поля ребенка могут зависеть от полей родителя. Чтобы поставить замок на TreasureBox, вам нужно найти переднюю часть коробки и вставить его туда. Если лицевая сторона ящика еще не создана, то как поставить на него замок?

Вот код, чтобы пояснить, что я имею в виду:

class Parent {

    public String parentField;

    public Parent(){
        parentField = "Hello";
    }
}

class Child extends Parent {

    public int childField = parentField.length();
}

Если Java использует ваш «интуитивный» порядок, будет выброшен NPE.

person Sweeper    schedule 28.01.2017
comment
Если вы сохраните одну схему для назначения только постоянных значений в инициализаторах полей и зависимых значений в конструкторе, между этими двумя порядками нет никакой разницы. В вашем примере кода используется противоположная схема. - person igrek51; 29.01.2017