Как Java проверяет, что final-поле инициализировано во всех конструкторах?
Компилятор применяет анализ определённого присваивания: к завершению каждого конструктора экземплярное final-поле должно получить значение ровно один раз на каждом возможном пути выполнения. Инициализация допустима непосредственно при объявлении поля, в инициализаторе экземпляра или в конструкторе.
Если существует путь, на котором поле не присваивается, компиляция завершается ошибкой. Если один и тот же путь может присвоить полю значение повторно, компилятор также отклоняет программу.
final-поля нужны для фиксации состояния объекта после его создания и для выражения неизменяемых атрибутов на уровне языка. Одной проверки запрета повторного присваивания было бы недостаточно: требовалось также гарантировать, что объект не завершит конструирование с неинициализированным обязательным полем.
Поэтому Java проверяет не только отдельные операторы присваивания, но и все ветви выполнения конструктора. Такой подход позволяет обнаруживать ошибку во время компиляции, не перенося её на момент использования объекта.
Рассмотрим объект, которому необходим обязательный параметр, например тайм-аут соединения. Если конструктор присваивает final-поле только в одной ветви условного оператора, часть объектов может быть создана без корректного значения.
Неверная реализация приводит к ошибке компиляции, а не к автоматической установке значения по умолчанию. Для ссылочного типа это особенно важно: значение null само по себе является допустимым значением поля, но не считается явной инициализацией final-поля конструктором.
Компилятор анализирует достижимость и состояние переменной на каждом пути. В следующем примере обе ветви присваивают полю значение, поэтому конструктор корректен:
Если убрать присваивание из одной ветви, появится ошибка вида «переменная могла быть не инициализирована». Если добавить безусловное присваивание после условного блока, компилятор обнаружит возможное повторное присваивание на тех путях, где поле уже получило значение.
Инициализация при объявлении выполняется до тела конструктора. Инициализатор экземпляра также участвует в общей последовательности инициализации. При этом присваивание должно происходить в разрешённом контексте конструктора или инициализатора, а не в обычном методе, даже если этот метод вызывается из конструктора.
Проверка выполняется статически и не анализирует произвольную семантику вызываемых методов. Компилятор не предполагает, что вспомогательный метод присвоит поле, поэтому обязательную инициализацию следует делать непосредственно в допустимом месте.
Главный компромисс — более строгий код конструктора против надёжности инвариантов. Иногда из-за сложной логики приходится вычислить значение во временной локальной переменной, проверить все ветви, а затем выполнить одно присваивание final-полю.
В классе конфигурации поле timeout обязательно для всех экземпляров, но его значение зависит от режима работы. Размещение логики в отдельном методе кажется удобным, однако присваивание final-поля из этого метода запрещено, а компилятор не сможет доказать корректность инициализации.
Возможны три варианта. Значение можно вычислять прямо в конструкторе: это прозрачно для анализа, но иногда увеличивает его объём. Можно использовать статический фабричный метод, который сначала вычисляет параметры, а затем передаёт готовое значение конструктору: код становится тестируемее, но появляется дополнительный API. Наконец, можно заменить final на обычное поле, однако это ослабит инвариант и позволит изменить конфигурацию после создания.
Практично оставить поле final, вынести сложное вычисление в функцию, возвращающую значение, и присвоить результат полю непосредственно в конструкторе. Так сохраняются проверка компилятора и неизменность состояния после создания объекта.
final-полю значение в обычном методе, если метод вызывается только из конструктора?Нет. Специальные правила разрешают присваивание экземплярному final-полю в объявлении, инициализаторе экземпляра или конструкторе. Вызов обычного метода не делает его частью доказуемого компилятором процесса инициализации, поэтому такое присваивание запрещается.
final-поля должно быть одинаковым во всех конструкторах?Нет. Разные конструкторы могут выбирать разные значения. Требование состоит не в одинаковости значения, а в том, чтобы каждый допустимый путь каждого конструктора присваивал поле ровно один раз до завершения конструирования.
final-поля отличается от проверки static final-поля?Экземплярное поле должно быть инициализировано для каждого создаваемого объекта, поэтому его проверка связана с конструкторами и инициализаторами экземпляра. static final-поле существует в единственном экземпляре на класс и инициализируется при объявлении либо в статическом инициализаторе. Конструкторы объектов не участвуют в его инициализации.