Почему при создании экземпляра потомка обязательно вызывается конструктор родителя?

Речь о Java. Чем технически объясняется тот факт, что при создании потомка всегда нужно вызывать конструктор родителя? Например, у нас два класса:

public class Asset {
    public String name;

    public Asset(String name) {
        this.name = name;
    }
}

public class House extends Asset {
    public long cost;

    public House(String name, long cost) {
        this.name = name;
        this.cost = cost;
    }
}

В House будет ошибка "There is no default constructor available in 'Asset'". Почему потомку так важен вызов родительского конструктора? Вот в данном конкретном случае это на первый взгляд вообще не нужно, т.к. у House конструктор заполняет все поля сам.

UPD. Вопрос не является дубликатом ни одной из ссылок, приведенных в комментариях. Ни один ответ \ комментарий \ ссылка не отвечает мне на вопрос, потому что ответы сводятся к нечто вроде:

  • "Для создания экземпляра подкласса требуется последовательно вызывать конструкторы суперклассов". Но не написано, почему это требуется.

Однако по фразе из ответа, ссылку на который дал Upiter 1401, "the superclass must be initialized before the subclass so that any initial values, memory locations, whatever have valid beginning values.", я сделал вывод, что нужно копать в сторону технической организации связи класса родителя и потомка. Например:

  • Каким образом технически реализуется наследование членов родителя в потомке. Т.е. это банальное закулисное копирование членов в потомка или все-таки какая-то более хитрая механика связи двух классов в памяти.
  • Когда создается экземпляр потомка, то загружается ли класс родителя в память.
  • Какова техническая роль конструктора в создании экземпляра, ведь инициализация полей происходит до отрабатывания конструктора. Стало быть это не основная причина того, что он всегда вызывается в конце процедуры создания экземпляра.
  • И т.д.

Ответы (4 шт):

Автор решения: peterz

Потому что у Вас поле Asset.name публичное, а если бы оно было приватным, его нельзя было бы проинициализировать в конструкторе подкласса.

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

→ Ссылка
Автор решения: insolor

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

Поэтому нужно:

  • или добавить соответствующий конструктор без параметров в родительский класс
  • или добавить явный вызов нужного конструктора с параметром (параметрами)
  • или убрать в родительском классе все конструкторы, тогда автоматически будет добавлен пустой конструктор без параметров (конструктор по умолчанию). Т.е. даже если вы в класс не добавите конструктор сами, конструктор в классе все равно будет.

Почему при создании экземпляра потомка обязательно вызывается конструктор родителя?

Потому что так реализовано в Java. В любом случае при наследовании будет вызвана цепочка конструкторов начиная от текущего конструктора до конструктора самого "верхнего" родителя (вполть до конструктора класса java.lang.Object).

→ Ссылка
Автор решения: AdamBeno

Когда вы создаете экземпляр класса, Java выполняет следующие шаги:

  1. Резервируется память для объекта, чтобы хранить его состояние (поля).
  2. Инициализируются поля объекта со значениями по умолчанию (например, числовые значения устанавливаются в 0, ссылки на объекты - в null).
  3. Вызывается конструктор класса для дополнительной инициализации объекта и установки начальных значений полей.

И принцип инкапсуляции в ООП подразумевает, что детали реализации класса скрыты от внешнего использования. Это означает, что производный класс не должен напрямую инициализировать поля, унаследованные от базового класса, поскольку это нарушает инкапсуляцию.

→ Ссылка
Автор решения: Alex Krass

Я так понимаю вас интересует логическое объяснение принципов устройства языка.

Конструктор по определению служит для инициализации объекта в рабочее согласованное состояние. Это значит, там находится критически важный код, который должен быть 100% выполнен до того, как объект класса может быть использован программой. Вы используете конструктор только как малую часть это большого механизма.

Вы используете его только для инициализации переменных, а можете и для большего:

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

Конструктор так же служит для того, чтобы этот код был выполнен независимо от хотения разработчика, использующего ваш класс. Это нужно, что бы не сломать его логику изнутри. Так же как и требования об обязательности приватных полей, инкапсуляции для охраны доступа к внутренним методам и т.д. То есть это повышает надежность программы, ведь обычные метод можно и забыть вызвать или по незнанию вовсе не вызвать.

Вторая часть, раз это критически важный код родительского класса, то он должен быть вызван и при наследовании. В противном случае наследование просто позволяло бы его игнорировать. А это означало бы нарушить принцип надежности программного обеспечение. Особенно учитывая, что у вас могут быть приватные поля и методы, доступ к которым нет даже у наследника. Как вы будете private проверять и обрабатывать дополнительной логикой? Или выполнять соединение с устройствами, если код находится уже в родителе (копипаста самый плохой выход)?

Третья часть, данные родительского класса должны быть проинициализированные и готовы к использованию остальным кодом. Дочерний класс, как и его конструктор, тоже должен получить правильное состояние всех данных перед их использованием. Поэтому конструктор родительского класса вызывается в самом начале. Только после этого программа может быть уверена, что вы работаете с правильными данными, дополняя и используя их в наследнике.

Это так же позволяет JVM внутри работать прозрачно с кодом и при надобности размещать инструкции там, где им место. Разработчику JVM не надо думать о том, где и как вы разместили данные, что обязательно вызывать и что по итогу нет. Реализация языка не занимается логическим интерпретированием, что вы там хотели написать, его задача четко выполнять инструкции.

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

UPD

Т.е. это банальное закулисное копирование членов в потомка или все-таки какая-то более хитрая механика связи двух классов в памяти.

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

Поэтому обычно реализацию смотрят на конкретных виртуальных машинах (тот же JVM HotSpot), с оговоркой, что в других виртуальных машинах может быть всё иначе.

→ Ссылка