Объясните, какая реализация переопределяемого экземплярного метода выбирается при его вызове из конструктора базового класса.
Выбирается реализация по фактическому классу объекта, то есть переопределённый метод класса-наследника. Конструктор базового класса выполняется первым, но это не отключает динамическое связывание экземплярных методов.
Из-за этого метод-наследник может выполниться до завершения конструктора наследника. Его поля ещё могут содержать значения по умолчанию, поэтому вызов переопределяемого метода из конструктора обычно считается опасным.
Полиморфизм в Java строится на динамическом связывании: код, работающий со ссылкой базового типа, может вызывать поведение фактического типа объекта. Это позволяет использовать наследников без изменения клиентского кода.
Конструкторы решают другую задачу — последовательную инициализацию объекта. В Java для конструкторов не введено отдельное правило, временно отключающее полиморфизм, поэтому обычные виртуальные вызовы сохраняют динамическое поведение даже во время построения объекта.
Конструктор базового класса может вызвать метод, который предназначен для переопределения. Если объект фактически относится к классу-наследнику, будет вызвана реализация наследника, хотя конструктор наследника ещё не начал выполнение.
В этот момент поля наследника уже получили значения по умолчанию, но их инициализаторы и тело конструктора ещё не выполнились. Ошибка может проявиться как исключение, некорректное состояние или публикация наружу частично созданного объекта.
При создании объекта сначала выделяется память под весь объект, затем выполняется цепочка конструкторов от базового класса к производному. Ссылка this во время конструктора базового класса уже представляет объект фактического производного типа.
Для переопределяемого экземплярного метода Java использует динамическое связывание. Поэтому вызов из конструктора базового класса направляется в наиболее специфичную переопределённую реализацию, доступную для фактического класса объекта.
При создании Child сначала запускается конструктор Base, который вызывает Child.describe(). Поле name ещё равно null, поэтому обращение к length() приведёт к NullPointerException.
Это правило относится именно к переопределяемым экземплярным методам. Статические, закрытые и final-методы не участвуют в обычном переопределении и не получают такое динамическое поведение; вызов super позволяет явно обратиться к реализации базового класса.
Практическое правило: конструктор не должен вызывать методы, которые могут быть переопределены. Безопаснее вызывать из него только private или final-методы, работающие с полностью инициализированным состоянием базового класса.
Базовый класс регистрирует объект в конструкторе через переопределяемый метод. Наследник переопределяет этот метод и использует конфигурацию, которая задаётся в его конструкторе. Регистрация запускается слишком рано: конфигурация ещё не установлена, а результат зависит от значения по умолчанию.
Вариант с сохранением вызова переопределяемого метода прост, но создаёт скрытую зависимость от порядка инициализации. Вариант с вызовом final-метода безопаснее, однако он не позволяет наследникам изменить алгоритм регистрации.
Можно также применить двухфазную инициализацию: сначала создать объект, затем явно вызвать метод завершения настройки. Это даёт нужный порядок, но требует не допустить использования объекта между фазами и усложняет контроль жизненного цикла.
Предпочтительное решение — убрать побочный вызов из конструктора и передавать готовые данные в отдельный сервис или фабрику. Фабрика сначала создаёт полностью настроенный объект, а затем выполняет регистрацию; в результате конструктор остаётся предсказуемым, а объект не наблюдается в частично инициализированном состоянии.
Меняет ли порядок инициализации выбор реализации метода?
Нет. Порядок инициализации определяет, когда состояние становится готовым, но не заменяет правило динамического связывания. Сначала выполняется конструктор базового класса, однако вызов переопределяемого метода всё равно может попасть в класс-наследник.
Что произойдёт с полем наследника, заданным инициализатором?
Во время конструктора базового класса такое поле обычно содержит значение по умолчанию для своего типа. Инициализаторы экземпляров и тело конструктора наследника выполняются только после завершения конструктора базового класса, поэтому строка ещё может быть null, число — 0, а ссылка на объект — null.
Как гарантированно вызвать базовую реализацию вместо переопределённой?
Внутри конструктора базового класса можно использовать вызов через super, если требуется именно реализация базового класса. Другой вариант — сделать вызываемый метод final или private, если переопределение концептуально не должно быть разрешено; это одновременно предотвращает неожиданный вызов кода наследника.