Как Java выбирает поле при обращении через переменную типа родителя, если потомок объявил поле с тем же именем?
Java выбирает поле по объявленному типу переменной, а не по фактическому типу объекта. Поля не переопределяются полиморфно: одинаковое имя в классе-потомке означает скрытие поля. Поэтому обращение через ссылку типа родителя использует поле родителя, даже если объект фактически создан как экземпляр потомка.
В Java отдельно определены правила для методов и полей. Переопределяемые экземплярные методы поддерживают динамический полиморфизм, а доступ к полям связывается статически, то есть на этапе компиляции.
Такое разделение сохраняет предсказуемость доступа к состоянию класса: выбор поля не зависит от того, какой объект оказался за ссылкой во время выполнения. Для полиморфного получения состояния обычно используют методы, а не открытые поля.
Скрытие поля легко принять за переопределение метода. Из-за этого один и тот же объект может выдавать разные значения при обращении к полю через ссылки разных статических типов.
Особенно опасна ситуация, когда поле используется как часть публичного API или участвует в логике базового класса. Изменение типа ссылки, явное приведение или вызов метода могут привести к обращению к разным полям, хотя объект остается тем же.
При компиляции Java смотрит на статический тип выражения, через которое выполняется доступ. Если переменная объявлена как тип родителя, выбирается поле, объявленное в родителе; если выражение имеет тип потомка, выбирается поле потомка.
Поля фактически существуют отдельно в каждой части объекта, соответствующей классу. У объекта потомка могут одновременно находиться поле родителя и скрывающее его поле потомка.
Приведение к Child не меняет объект, а только статический тип выражения, поэтому меняется выбранное поле. Вызов переопределенного метода в аналогичной ситуации подчиняется другому правилу: для экземплярного метода реализация выбирается по фактическому типу объекта.
Если поле объявлено как static, оно также выбирается по типу выражения, но связано с классом, а не с экземпляром. Статические поля рекомендуется обращаться через имя класса, чтобы не создавать ложного впечатления динамического полиморфизма.
Скрытие полей обычно является плохим проектным решением: оно повышает связанность и затрудняет сопровождение. Если состояние должно полиморфно изменяться или вычисляться, предпочтительнее сделать поле закрытым и предоставить переопределяемый метод доступа.
В базовом классе модели находилось открытое поле status, а в подклассе появилось поле с тем же именем. Часть кода принимала объект как базовый тип, а другая — как тип подкласса; в результате журналирование и бизнес-проверки читали разные значения одного внешне одинакового свойства.
Рассматривались три варианта. Оставить поля открытыми было проще, но сохраняло неоднозначность. Явно приводить тип перед каждым обращением устраняло отдельные симптомы, однако делало код хрупким и связывало его с конкретным подклассом. Заменить поля на закрытое состояние и методы доступа потребовало изменить API, но обеспечило единое полиморфное поведение.
Выбран был третий вариант: состояние хранилось в одном поле базового класса, а различия поведения реализовывались переопределением метода. Это устранило расхождения при передаче объектов через базовые типы и сделало контракт класса понятным.
1. Можно ли считать поле переопределенным, если в потомке объявлено поле с тем же именем?
Нет. Термин переопределение применяется к совместимому экземплярному методу, когда вызов может выбрать реализацию по фактическому типу объекта. Для полей используется термин скрытие: поля родителя и потомка остаются разными членами, а выбор определяется статическим типом выражения.
2. Что произойдет при вызове метода родителя, который обращается к скрытому полю?
Метод родителя обращается к полю, определенному в контексте родителя. Даже если метод вызван на объекте потомка, его неквалифицированное обращение к такому полю не переключается на поле потомка.
Это отличается от вызова переопределенного метода внутри того же метода: такой вызов может динамически перейти в реализацию потомка. Поэтому поле и метод с одинаковым именем не образуют единого полиморфного свойства.
3. Как следует проектировать классы, если значение должно зависеть от фактического типа объекта?
Не следует рассчитывать на скрытие полей или на приведения типов. Состояние обычно делают private, а доступ оформляют методом, который можно переопределить в потомке, либо используют композицию и явную стратегию поведения.
У методов есть собственные ограничения: переопределить можно только метод, совместимый по сигнатуре и правилам доступа, а static, private и final методы не дают обычного динамического переопределения. Тем не менее такой дизайн обычно яснее, чем несколько одноименных полей, выбор которых зависит от статического типа ссылки.