Чем различаются правила начальной инициализации полей и локальных переменных в Java?
Поля классов получают значения по умолчанию до выполнения конструктора: числовые поля — 0, boolean — false, ссылочные — null. Локальная переменная значения по умолчанию не получает: перед чтением компилятор требует доказать, что ей присвоено значение на каждом возможном пути выполнения.
Это правило называется definite assignment — гарантированное присваивание. Оно предотвращает чтение логически неинициализированной локальной переменной, тогда как поля всегда имеют начальное состояние объекта.
Объект в Java должен находиться в предсказуемом начальном состоянии ещё до выполнения тела конструктора. Поэтому виртуальная машина автоматически обнуляет память нового объекта, а затем запускается инициализация полей и тело конструктора.
Для локальных переменных такой механизм не нужен: они существуют только в рамках выполнения метода, а компилятор может заранее проанализировать пути управления. Проверка на этапе компиляции позволяет обнаружить ошибку до запуска программы, не вводя скрытые значения для каждой локальной переменной.
Если переменная читается после присваивания только в одной ветви условного оператора, другая ветвь может привести к чтению без значения. Автоматическая подстановка 0 или null скрывала бы такую ошибку и могла бы приводить к некорректным результатам гораздо дальше от места возникновения проблемы.
Для полей риск другой: значение по умолчанию существует всегда, но оно может быть временно не тем, которое требуется бизнес-логике. Поэтому конструктор обязан явно установить необходимые значения, если 0, false или null не являются допустимым состоянием.
Проверка локальной переменной выполняется по графу управления. Перед чтением переменная должна быть определённо присвоена на всех путях, которые могут привести к этой точке.
Поле field доступно сразу и содержит 0. Локальная переменная local становится доступной после условного оператора, потому что присваивание выполнено в каждой его ветви; если убрать else, компилятор отвергнет чтение local.
Для полей порядок таков: память объекта получает значения по умолчанию, затем выполняются значения инициализаторов и блоков инициализации, после чего — тело конструктора. У локальной переменной инициализатор обязателен, если она должна быть прочитана.
Правило распространяется также на локальные final-переменные. Им можно присвоить значение позже объявления, но ровно один раз на каждом допустимом пути; повторное присваивание после гарантированного первого запрещено.
Проверка является консервативной: компилятор анализирует синтаксически возможные пути, а не пытается доказать все фактические значения условий. Поэтому даже очевидное для разработчика условие может потребовать явной инициализации.
В обработчике запроса нужно вычислить статус, причём один вариант зависит от входных данных, а другой — от результата проверки прав. Разработчик объявляет локальную переменную перед условием, присваивает её только при успешной проверке, а затем использует после условия.
Вариант с присваиванием только в успешной ветви сразу отклоняется компилятором. Инициализация значением по умолчанию устраняет ошибку компиляции, но может замаскировать пропущенную ветвь и привести к неверному статусу. Дублирование return в каждой ветви делает поток явным, однако может ухудшить читаемость при сложной общей логике.
Предпочтительное решение — присвоить результат во всех ветвях либо вынести вычисление в отдельный метод, возвращающий значение по каждому сценарию. Это сохраняет гарантию корректной инициализации без использования фиктивного значения, смысл которого легко перепутать с настоящим результатом.
Можно ли прочитать локальную переменную в finally, если она присваивается внутри try?
Только если компилятор гарантирует присваивание на каждом пути, ведущем к finally. Исключение может возникнуть до строки присваивания, после чего управление всё равно перейдёт в finally; поэтому переменная, присваиваемая лишь внутри try, обычно не считается определённо присвоенной в finally.
Почему поле можно прочитать из метода до явного присваивания в конструкторе?
Потому что память объекта уже инициализирована значением по умолчанию до начала тела конструктора. Это не означает, что объект находится в корректном бизнес-состоянии: поле может временно содержать null или 0. Дополнительная проблема возникает, если конструктор передаёт ссылку на ещё не завершённый объект наружу: другой код может увидеть эти промежуточные значения.
Почему компилятор не считает условие всегда истинным, если это очевидно из логики программы?
Проверка definite assignment основана на формальных правилах анализа потока управления, а не на полном доказательстве поведения программы. Компилятор не обязан выводить сложные зависимости между значениями переменных, вызовами методов и внешним состоянием. Поэтому безопаснее явно организовать ветви так, чтобы присваивание было видно во всех путях.