Каков порядок вызова конструкторов и блоков инициализации с учетом иерархии классов

Обновлено: 19.04.2024

Первое что произойдет — проинициализируются статические переменные класса Car . Да-да, именно класса Car , а не Truck . Статические переменные инициализируются еще до вызова конструкторов, и начинается это в классе-родителе. Давай попробуем проверить. Выставим счетчик carCounter в классе Car на 10 и попробуем вывести его в консоль в обоих конструкторах — Car и Truck .

Мы специально поставили вывод в консоль в самом начале конструктора Truck , чтобы точно знать: поля грузовика на момент вывода carCounter ’a в консоль еще не были инициализированы.

А вот и результат:

После инициализации статических переменных класса-предка инициализируются статические переменные класса-потомка. То есть в нашем случае — поле truckCounter класса Truck .

Опять же, проведем эксперимент и попробуем вывести значение truckCounter ’a внутри конструктора Truck до инициализации остальных полей:

Как видишь, значение 10 уже было присвоено нашей статической переменной на момент, когда конструктор Truck начал свою работу.

Время конструкторов все еще не пришло! Инициализация переменных продолжается. Третьими по счету будут инициализированы нестатические переменные класса-предка. Как видишь, наследование заметно усложняет процесс создания объекта, но тут уж ничего не поделаешь: некоторые вещи в программировании придется просто запомнить :)

Для эксперимента мы можем присвоить переменной description класса Car какое-то первоначальное значение, а потом поменять его в конструкторе.

Запустим наш метод main() с созданием грузовика:

И получим результат:

Это доказывает, что на момент начала работы конструктора Car у поля description уже было присвоенное значение.

Наконец, дело дошло до конструкторов! Точнее, до конструктора базового класса. Начало его работы — четвертый пункт в процессе создания объекта.

Проверить это тоже достаточно легко. Попробуем вывести в консоль две строки: одну внутри конструктора базового Car , вторую — внутри конструктора Truck . Нам нужно убедиться, что строка внутри Car выведется первой:

Запускаем наш метод main() и смотрим на результат:

Отлично, значит мы не ошиблись :) Едем дальше.

Теперь пришла очередь инициализации нестатических полей класса-потомка , то есть нашего класса Truck . Поля класса, объект которого мы создаем, инициализируются только в пятую очередь! Удивительно, но факт :) Опять же, проведем простую проверку, такую же, как и с родительским классом: присвоим переменной maxSpeed некоторое изначальное значение и проверим в конструкторе Truck , что оно было присвоено раньше, чем конструктор начал работу:

Вопросы и ответы по теме ООП (объектно ориентированное программирование) для собеседования по Java.

К списку вопросов по всем темам

Список всех вопросов по ООП

1. Назовите принципы ООП и расскажите о каждом.
2. Дайте определение понятию “класс”.
3. Что такое поле/атрибут класса?
4. Как правильно организовать доступ к полям класса?
5. Дайте определение понятию “конструктор”.
6. Чем отличаются конструкторы по умолчанию, копирования и конструктор с параметрами?
7. Какие модификации уровня доступа вы знаете, расскажите про каждый из них.
8. Расскажите об особенностях класса с единственным закрытым (private) конструктором.
9. О чем говорят ключевые слова “this”, “super”, где и как их можно использовать?
10. Дайте определение понятию “метод”.
11. Что такое сигнатура метода?
12. Какие методы называются перегруженными?
13. Могут ли нестатические методы перегрузить статические?
14. Расскажите про переопределение методов.
15. Может ли метод принимать разное количество параметров (аргументы переменной длины)?
16. Можно ли сузить уровень доступа/тип возвращаемого значения при переопределении метода?
17. Как получить доступ к переопределенным методам родительского класса?
18. Какие преобразования называются нисходящими и восходящими?
19. Чем отличается переопределение от перегрузки?
20. Где можно инициализировать статические/нестатические поля?

21. Зачем нужен оператор instanceof?
22. Зачем нужны и какие бывают блоки инициализации?
23. Каков порядок вызова конструкторов и блоков инициализации двух классов: потомка и его предка?
24. Где и для чего используется модификатор abstract?
25. Можно ли объявить метод абстрактным и статическим одновременно?
26. Что означает ключевое слово static?
27. К каким конструкциям Java применим модификатор static?
28. Что будет, если в static блоке кода возникнет исключительная ситуация?
29. Можно ли перегрузить static метод?
30. Что такое статический класс, какие особенности его использования?
31. Какие особенности инициализации final static переменных?
32. Как влияет модификатор static на класс/метод/поле?
33. О чем говорит ключевое слово final?
34. Дайте определение понятию “интерфейс”.
35. Какие модификаторы по умолчанию имеют поля и методы интерфейсов?
36. Почему нельзя объявить метод интерфейса с модификатором final или static?
37. Какие типы классов бывают в java (вложенные… и.т.д.)
38. Какие особенности создания вложенных классов: простых и статических.
39. Что вы знаете о вложенных классах, зачем они используются? Классификация, варианты использования, о нарушении инкапсуляции.
40. В чем разница вложенных и внутренних классов?
41. Какие классы называются анонимными?
42. Каким образом из вложенного класса получить доступ к полю внешнего класса?

43. Каким образом можно обратиться к локальной переменной метода из анонимного класса, объявленного в теле этого метода? Есть ли какие-нибудь ограничения для такой переменной?
44. Как связан любой пользовательский класс с классом Object?
45. Расскажите про каждый из методов класса Object.
46. Что такое метод equals(). Чем он отличается от операции ==.
47. Если вы хотите переопределить equals(), какие условия должны удовлетворяться для переопределенного метода?
48. Если equals() переопределен, есть ли какие-либо другие методы, которые следует переопределить?
49. В чем особенность работы методов hashCode и equals? Каким образом реализованы методы hashCode и equals в классе Object? Какие правила и соглашения существуют для реализации этих методов? Когда они применяются?
50. Какой метод возвращает строковое представление объекта?
51. Что будет, если переопределить equals не переопределяя hashCode? Какие могут возникнуть проблемы?
52. Есть ли какие-либо рекомендации о том, какие поля следует использовать при подсчете hashCode?
53. Как вы думаете, будут ли какие-то проблемы, если у объекта, который используется в качестве ключа в hashMap изменится поле, которое участвует в определении hashCode?
54. Чем отличается абстрактный класс от интерфейса, в каких случаях что вы будете использовать?
55. Можно ли получить доступ к private переменным класса и если да, то каким образом?
56. Что такое volatile и transient? Для чего и в каких случаях можно было бы использовать default?
57. Расширение модификаторов при наследовании, переопределение и сокрытие методов. Если у класса-родителя есть метод, объявленный как private, может ли наследник расширить его видимость? А если protected? А сузить видимость?
58. Имеет ли смысл объявлять метод private final?
59. Какие особенности инициализации final переменных?
60. Что будет, если единственный конструктор класса объявлен как final?
61. Что такое finalize? Зачем он нужен? Что Вы можете рассказать о сборщике мусора и алгоритмах его работы.
62. Почему метод clone объявлен как protected? Что необходимо для реализации клонирования?

Ответы. Часть 1

1. Назовите принципы ООП и расскажите о каждом.

Объе́ктно-ориенти́рованное программи́рование (ООП) — это методология программирования, основанная на представлении программы в виде совокупности объектов, каждый из которых является экземпляром определенного класса, а классы образуют иерархию наследования.

Основные принципы ООП: абстракция, инкапсуляция, наследование, полиморфизм.

Абстракция — означает выделение значимой информации и исключение из рассмотрения незначимой. С точки зрения программирования это правильное разделение программы на объекты. Абстракция позволяет отобрать главные характеристики и опустить второстепенные.

Пример: описание должностей в компании. Здесь название должности значимая информация, а описание обязанностей у каждой должности это второстепенная информация. К примеру главной характеристикой для «директор» будет то, что это должность чем-то управляет, а чем именно (директор по персоналу, финансовый директор, исполнительный директор) это уже второстепенная информация.

Инкапсуляция — свойство системы, позволяющее объединить данные и методы, работающие с ними, в классе. Для Java корректно будет говорить, что инкапсуляция это «сокрытие реализации». Пример из жизни — пульт от телевизора. Мы нажимаем кнопочку «увеличить громкость» и она увеличивается, но в этот момент происходят десятки процессов, которые скрыты от нас. Для Java: можно создать класс с 10 методами, например вычисляющие площадь сложной фигуры, но сделать из них 9 private. 10й метод будет называться «вычислитьПлощадь()» и объявлен public, а в нем уже будут вызываться необходимые скрытые от пользователя методы. Именно его и будет вызывать пользователь.

Наследование — свойство системы, позволяющее описать новый класс на основе уже существующего с частично или полностью заимствующейся функциональностью. Класс, от которого производится наследование, называется базовым, родительским или суперклассом. Новый класс — потомком, наследником, дочерним или производным классом.

Полиморфизм — свойство системы использовать объекты с одинаковым интерфейсом без информации о типе и внутренней структуре объекта. Пример (чуть переделанный) из Thinking in Java:

Есть тестовое задание с вопросом: "Что напечатает следующий код?" При запуске данного кода в IDE получаю следующую последовательность: 14725689 Вопрос следующий : есть какой-то порядок при котором инициализируются статические блоки/нестатические/конструкторы при наследовании?

И собственно почему именно такая последовательность получилась?

4 ответа 4

Немного расширю предыдущий ответ

Поля объекта инициализируются в следующем порядке:

  1. Статические поля класса Parent;
  2. Статический блок инициализации класса Parent;
  3. Статические поля класса Сhild;
  4. Статический блок инициализации класса Child;
  5. Нестатические поля класса Parent;
  6. Нестатический блок инициализации класса Parent;
  7. Конструктор класса Parent;
  8. Нестатические поля класса Сhild;
  9. Нестатический блок инициализации класса Сhild;
  10. Конструктор класса Сhild.

Сначала несколько важных замечаний.
1) Если в классе не создать конструктор, будет создан конструктор по умолчанию (без аргументов), со спецификатором доступа, как у класса:

2)Первой командой конструктора должна быть this(для вызова другого конструктора) или super(для вызова конструктора суперкласса). Еслу этой команды нет - компилятор добавит ее сам. Следующий код не скомпилируется т.к. в суперклассе создан конструктор с аргументом и конструктор поумолчанию не будет создан, при этом в конструкторе подкласса нет super или this, следовательно компилятор добавляет super() - вызов несуществующего конструктора без аргумента.

В вашем примере порядок будет следующий:
1. При вызове конструктора new C(), сначал производится инициализация статических членов и выполнение блоков статической инициализации. Т.к. класс имеет суперклассы, сначала инициализируются статические члены суперкласса от базового и вниз по иерархии. При этом:
1.1 Выполняется static блок в классе A - выводится "1"
1.2 Выполняется static блок в классе B - выводится "4"
1.3 Выполняется static блок в классе С - выводится "7"
2. По цепочке вызовов приходим к конструктору базового класса:
C() -> В(String str) -> B() -> A() <. >
2.1 В базовом классе A инициализируются поля, в порядке их определения и вызаваются блоки инициализации <. >
2.2 Выполняется тело конструктор A() - выводится "2"
2.3 В классе B инициализируются поля, в порядке их определения и вызаваются блоки инициализации <. >(у вас в данном классе полей и блоков нет)
2.4 Выполняется тело конструктора B()- выводится "5"
2.5 Исполнение возвращается в конструктор B(String str)(см. пункт 2), выполняется тело конструктора - выводится "6"
2.6 В классе С инициализируются поля, в порядке их определения и вызаваются блоки инициализации <. >- выводтся "8"
2.7 Выполняется тело конструктора класса С - С()- выводится "9"

Нашла следующий порядок инициализации объекта для случая наследования классов:

  • Статические поля класса Parent;
  • Статический блок инициализации класса Parent;
  • Статические поля класса Сhild;
  • Статический блок инициализации класса Child;
  • Нестатические поля класса Parent;
  • Нестатический блок инициализации класса Parent;
  • Конструктор класса Parent;
  • Нестатические поля класса Сhild;
  • Нестатический блок инициализации класса Сhild
  • Конструктор класса Сhild.

Но что делать, если несколько конструкторов и в родителе, и в наследнике - каким образом они загружаются? Все конструкторы родителя, а потом наследника или по одному в порядке написания?

На собеседовании мне показали пример возможной ошибки в конструкторе (или его исполнения - точно не помню) наследника из-за порядка конструкторов родителя, но я не могу найти нигде внятной информации и примеров такой ситуации в коде. Можете разъяснить этот момент?

"все конструкторы родителя" . - выполняется только тот конструктор, который вызывается в коде. Если один конструктор вызывает другой, то в этой цепочке все однозначно.

вы можете показать пример использования/взаимодействия конструкторов родителя и наследника, при котором возникнет исключительная ситуация?

В знаете в каких случаях может возникнуть исключение при использовании конструкторов наследником родителя ? (имеет отношение к порядку загрузки классов и их элементов)

2 ответа 2

В классе-наследнике вызывается один конструктор родителя.
Либо конструктор родителя вызывается явно в начале конструктора наследника с помощью конструкции super , либо вызывается неявно при подстановке super(); в начало конструктора компилятором. При этом если у родителя нет конструктора без аргументов, то код приведёт к ошибке компиляции.
Документация по этому поводу.

Таким образом, нет неопределённости с тем, сколько конструкторов родителя и какие именно вызываются в потомке.

Проблемы из-за порядка вызова конструкторов и инициализации полей классов могут возникнуть, как писал @zRrr, если в конструкторе родителя вызывается переопределяемый или реализуемый потомком метод, который в свою очередь использует ещё не инициализированные поля потомка.
Пример:

На экран будет выведено Animal null , так как на момент вызова метода getName у Dog поле thisName ещё не инициализировано.
При создании animal происходит вызов конструктора по умолчанию у Dog , который приводит сначала к вызову конструктора без параметров у Animal , а только затем к инициализации поля thisName .

Ещё есть такой пример, взятый из этой статьи:

На экран будет выведено:

Но если заменить String lowerString = null; на String lowerString; , то вывод будет:

Проблема в первоначальном варианте в том, что присваивание null в String lowerString = null; происходит после вызова конструктора родителя, в котором в свою очередь вызывается метод Initializer.initialize . То есть в lowerString сначала записывается "lowerInited" , а затем null .

Оба примера демонстрируют проблемы, возникающие из-за неправильных предположений о порядке выполнения кода. Сложность осознания того, что за чем выполняется в данном коде, сама по себе уже говорит о том, что на практике код так писать не стоит.

Читая книгу Эккеля, натолкнулся на один момент, который не могу понять. Представлен следующий код:

Всегда думал, что перед вызовом конструктора, должны проиницилизоваться поля класса, чтобы в случае обращения к неициализирвоанной переменной в конструкторе не получить исключение. Следуя этой логике вывод на экран надписи "Beetle.k initialized" должен был быть после вывода "Beetle constructor" . Прощу помочь разобраться. Спасибо.

3 ответа 3

Порядок инициализации таков:

  1. Статические элементы родителя
  2. Статические элементы наследника
  3. Глобальные переменные родителя
  4. Конструктор родителя
  5. Глобальные переменные наследника
  6. Конструктор наследника

Пример

Еще было бы неплохо указать, что статические переменные(да и вообще весь статический контент) инициализируется когда классЛоадер впервые натыкается на этот класс. А все другие переменные уже при вызову конструктора.

@Nara всё верно. Если это не делается вручную, то компилятор сам прописывает вызов конструктора родителя без аргументов.

@Nara, вас смутило то, что я в порядке указал конструктор родителя раньше наследника? Дело в том, что первым делом, при чтении конструктора наследника, вызывается конструктор родителя. Так что справедливее писать именно так.

Порядок инициализации экземпляра объекта описан в JLS (12.5 Creation of New Class Instances) (перевод мой):

  1. Присвоить аргументы конструктора переменным-параметрам для вызова этого конструктора.
  2. Если конструктор начинается с явного вызова (§8.8.7.1) другого конструктор этого же класса (с использованием this ), то нужно вычислить аргументы и выполнить тот конструктор рекурсивно, используя эти же 5 шагов. Если выполнение того конструктора будет прервано (completes abruptly), то этот алгоритм будет прерван по тем же причинам, иначе перейти к шагу 5.
  3. Если конструктор не начинается с явного вызова другого конструктора этого же класса (с использованием this ), то для классов, отличных от Object явно или неявно вызывается конструктор суперкласса (используя super ). Нужно вычислить аргументы и выполнить конструктор суперкласса рекурсивно, используя эти же 5 шагов. Если выполнение того конструктора будет прервано (completes abruptly), то этот алгоритм будет прерван по тем же причинам, иначе перейти к шагу 4.
  4. Выполнить инициализаторы экземпляра (instance initializers) и инициализаторы переменных экземпляра (instance variable initializers) для этого класса, с присвоением значений инициализаторов переменных экземпляра соответствующим переменным экземпляра, слева на право в порядке появления в исходном коде класса. Если выполнение любого инициализатора вызывает исключение, следующие инициализаторы не обрабатываются, а этот алгоритм завершается с тем же исключением. Иначе перейти к шагу 5.
  5. Выполнить остальное тело конструктора. Если выполнение будет прервано, то этот алгоритм будет прерван по тем же причинам. Иначе алгоритм завершится нормально.
  • T - класс и создается экземпляр класса T .
  • Вызов статического метода, объявленного в T .
  • Выполняется присваивание статическому полю, объявленному в T .
  • Статическое поле, объявленное в T используется, и это поле не является константой (§4.12.4) (константа - final переменная примитивного типа или String , объявленная с инициализатором, являющимся константным выражением)
  • T - класс верхнего уровня (§7.6) и выполняется выражение assert (assert statement) (§14.10) лексически расположенное внутри T (§8.1.3).

Вооружившись этим корявым переводом, посмотрим, что происходит в вашем примере:

Читайте также: