К каким последствиям приводит вызов переопределяемого метода из конструктора Java-класса?
Вызов переопределяемого метода из конструктора выполняется полиморфно: если объект создаётся подклассом, будет вызвана реализация подкласса. При этом поля и конструктор подкласса ещё не инициализированы, поэтому метод может увидеть значения по умолчанию или вызвать ошибку. Такой вызов обычно считается опасным и его следует избегать.
Динамическая диспетчеризация была заложена в объектную модель Java для работы с объектами через базовый тип: фактическая реализация переопределённого метода выбирается по типу объекта во время выполнения. Конструктор при этом предназначен для последовательного формирования состояния объекта, поэтому совмещение конструирования с полиморфным вызовом создаёт особый риск.
Java не отключает полиморфизм во время выполнения конструктора. Это сохраняет единые правила переопределения методов, но требует от разработчика учитывать незавершённую инициализацию объекта.
При создании объекта сначала выполняется конструктор базового класса, а затем инициализаторы полей и конструктор подкласса. Если базовый конструктор вызывает переопределённый метод, реализация подкласса запускается до завершения этих этапов.
В результате метод подкласса может прочитать ещё не присвоенное поле, передать некорректное состояние в другой объект или выбросить исключение. Ошибка часто выглядит неожиданно: синтаксически создание объекта корректно, но его состояние зависит от порядка конструирования.
Для обычного экземплярного метода Java использует динамическую диспетчеризацию. Поэтому вызов из конструктора базового класса не означает автоматический выбор реализации базового класса: если метод переопределён, вызывается версия фактического класса объекта.
До выполнения конструктора подкласса его поля уже существуют, но содержат значения по умолчанию: числовые поля равны нулю, логические — false, ссылочные — null. Явные значения полей инициализируются позже, поэтому рассчитывать на них внутри такого вызова нельзя.
При создании Child из конструктора Base будет вызвана версия printState из Child, но state ещё равен null, а не "ready". Аннотация @Override только проверяет корректность переопределения на этапе компиляции и не меняет правила диспетчеризации.
Особенно опасны вызовы методов, которые публикуют this, регистрируют объект, запускают фоновые задачи или обращаются к другим ещё не инициализированным полям. Даже если сегодня переопределения нет, добавление подкласса позже может сделать существующий конструктор небезопасным.
Для безопасной логики конструирования используют private или final методы, если им не требуется полиморфизм, либо переносят вызов переопределяемого метода в фабричный метод после завершения конструктора. Компромисс состоит в том, что фабрика усложняет создание объекта, зато позволяет гарантировать полностью инициализированное состояние.
В базовом классе отчёта конструктор вызывал метод configureFormat, чтобы подкласс мог выбрать формат вывода. Один из подклассов хранил формат в поле, задаваемом инициализатором, но при вызове из конструктора базового класса получал null и создавал некорректную конфигурацию.
Рассматривались три варианта. Оставить виртуальный вызов было проще всего, но небезопасно. Сделать метод final и использовать только состояние базового класса было надёжно, однако это исключало настройку в подклассах. Перенести настройку в статический фабричный метод позволяло сохранить полиморфизм, но добавляло отдельный этап создания.
Выбрали фабричный метод: сначала конструктор полностью создавал объект, затем фабрика вызывала настройку. Это устранило зависимость от порядка инициализации и сохранило возможность разных реализаций для подклассов.
Нет. Память под поля уже выделена, но инициализаторы полей подкласса выполняются после завершения конструктора базового класса. Поэтому метод увидит значения по умолчанию, а не значения, записанные в объявлениях полей.
super?Нет, само наличие вызова super не устраняет проблему. Сначала действительно выполнится базовая реализация, но затем продолжится код метода подкласса, который всё ещё работает до завершения инициализации подкласса. Кроме того, базовая реализация сама может обратиться к состоянию, ещё не подготовленному для конкретного подкласса.
Конструирование завершится с исключением, и ссылка на корректно созданный объект не будет возвращена вызывающему коду. Частично инициализированный объект может быть доступен побочным участникам, если конструктор или вызванный метод успел опубликовать this; это создаёт дополнительные проблемы с согласованностью состояния и безопасностью многопоточного доступа.