Вызов конструктора базового класса из конструктора класса наследника
Наследование (inheritance) представляет один из ключевых аспектов объектно-ориентированного программирования, который позволяет наследовать функциональность одного класса или базового класса (base class) в другом - производном классе (derived class).
Зачем нужно наследование? Рассмотрим небольшую ситуацию, допустим, у нас есть классы, которые представляют человека и работника предприятия:
В данном случае класс Employee фактически содержит функционал класса Person: свойства name и age и функцию display. И было бы не совсем правильно повторять функциональность одного класса в другом классе, тем более что по сути сотрудник предприятия в любом случае является человеком. Поэтому в этом случае лучше использовать механизм наследования. Унаследуем класс Employee от класса Person:
Для установки отношения наследования после название класса ставится двоеточие, затем идет название класса, от которого мы хотим унаследовать функциональность. В этом отношении класс Person еще будет называться базовым классом, а Employee - производным классом.
Перед названием базового класса также можно указать спецификатор доступа, как в данном случае используется спецификатор public , который позволяет использовать в производном классе все открытые члены базового класса. Если мы не используем модификатор доступа, то класс Employee ничего не будет знать о переменных name и age и функции display.
После установки наследования мы можем убрать из класса Employee те переменные, которые уже определены в классе Person. Используем оба класса:
Таким образом, через переменную класса Employee мы можем обращаться ко всем открытым членам класса Person.
Конструкторы
Стоит учитывать, что конструкторы при наследовании не наследуются. И если базовый класс содержит только конструкторы с параметрами, то производный класс должен вызывать в своем конструкторе один из конструкторов базового класса. Например, добавим в классы выше конструкторы:
После списка параметров конструктора производного класса через двоеточие идет вызов конструктора базового класса, в который передаются значения параметров n и a.
Если бы мы не вызвали конструктор базового класса, то это было бы ошибкой.
Консольный вывод программы:
Таким образом, в строке
Вначале будет вызываться конструктор базового класса Person, в который будут передаваться значения "Bob" и 31. И таким образом будут установлены имя и возраст. Затем будет выполняться собственно конструктор Employee, который установит компанию.
Также мы могли бы определить конструктор Employee следующим обазом:
Также в примере выше стои отметить, что переменные в обоих классах стали закрытыми, то есть они объявлены со спецификатором private. Производный класс не может обращаться к закрытым членам базового класса. Поэтому, если бы мы попробовали обратиться к закрытым переменным класса Person через переменную класса Employee, то мы бы получили ошибку:
Спецификатор protected
С помощью спецификатора public можно определить общедоступные открытые члены классы, которые доступны извне и их можно использовать в любой части программы. С помощью спецификатора private можно определить закрытые переменные и функции, которые можно использовать только внутри своего класса. Однако иногда возникает необходимость в таких переменных и методах, которые были бы доступны классам-наследникам, но не были бы доступны извне. И именно для определения уровня доступа подобных членов класса используется спецификатор protected .
Например, определим переменную name со спецификатором protected:
Таким образом, мы можем использовать переменную name в производном классе, например, в методе showEmployeeName, но извне мы к ней обратиться по-прежнему не можем.
Запрет наследования
Иногда наследование от класса может быть нежелательно. И с помощью спецификатора final мы можем запретить наследование:
После этого мы не сможем унаследовать другие классы от класса User. И, например, если мы попробуем написать, как в случае ниже, то мы столкнемся с ошибкой:
Если в определении ничего про базовый конструктор не писать, то компилятор в принципе и не ругается. Как надо-то? В инете полно инфы как их наследовать, но не могу найти про вынесение всего этого за пределы класса. Тоже самое с конструктором копирования. И как быть с перегруженными операторами(например присваивания), их виртуальными просто сделать?
Да у меня и дома есть учебник Лафоре по ООП, который я изучил еще полгода назад. Пересмотрел кучу видео-курсов, которые как пишут в комментариях полностью повторяют учебники. Смотрел университетские лекции в том числе про препроцессор, объектные файлы, линковку. И такие нюансы там не объясняют. И я даже не знаю в каком разделе учебника это искать. Наследование и вынос конструкторов за пределы класса это две совершенно разные темы. Возможно я просто чего-то не понял. Вы бы еще сказали "а разве у вас в городе нет ВУЗов?" Всего пять лет обучения и ошибка в программе исправлена))
2 ответа 2
Если в определении ничего про базовый конструктор не писать, то компилятор в принципе и не ругается.
Странное и неверное заявление. Базовый конструктор тут вообще ни при чем. В заголовочный файл для класса Category вы поместили инициализацию базового класса и тела конструкторов в виде <> . В файл реализации вы снова поместили инициализацию базового класса и еще одни тела ваших конструкторов в виде < что-то >. Этим нарушено Правило Одного Определения. У одной и той же функции не может быть два тела. Зачем вы два раза определяете тело для каждого конструктора?
Либо помещайте определения в заголовочный файл, либо в файл реализации. Вот и все. Все это должно присутствовать только в одном месте.
Ваши определения конструкторов в файле реализации выглядят нормально. Зачем вы тогда написали инициализацию базового класса и какие-то <> в заголовочном файле?
И как быть с перегруженными операторами(например присваивания), их виртуальными просто сделать?
Здесь вообще непонятно о чем идет речь. При чем здесь виртуальность вообще?
Наследование (inheritance) является одним из ключевых моментов ООП. Благодаря наследованию один класс может унаследовать функциональность другого класса.
Пусть у нас есть следующий класс Person, который описывает отдельного человека:
Но вдруг нам потребовался класс, описывающий сотрудника предприятия - класс Employee. Поскольку этот класс будет реализовывать тот же функционал, что и класс Person, так как сотрудник - это также и человек, то было бы рационально сделать класс Employee производным (или наследником, или подклассом) от класса Person, который, в свою очередь, называется базовым классом или родителем (или суперклассом):
После двоеточия мы указываем базовый класс для данного класса. Для класса Employee базовым является Person, и поэтому класс Employee наследует все те же свойства, методы, поля, которые есть в классе Person. Единственное, что не передается при наследовании, это конструкторы базового класса.
Таким образом, наследование реализует отношение is-a (является), объект класса Employee также является объектом класса Person:
И поскольку объект Employee является также и объектом Person, то мы можем так определить переменную: Person p = new Employee() .
По умолчанию все классы наследуются от базового класса Object , даже если мы явным образом не устанавливаем наследование. Поэтому выше определенные классы Person и Employee кроме своих собственных методов, также будут иметь и методы класса Object: ToString(), Equals(), GetHashCode() и GetType().
Все классы по умолчанию могут наследоваться. Однако здесь есть ряд ограничений:
Не поддерживается множественное наследование, класс может наследоваться только от одного класса.
При создании производного класса надо учитывать тип доступа к базовому классу - тип доступа к производному классу должен быть таким же, как и у базового класса, или более строгим. То есть, если базовый класс у нас имеет тип доступа internal , то производный класс может иметь тип доступа internal или private , но не public .
Однако следует также учитывать, что если базовый и производный класс находятся в разных сборках (проектах), то в этом случае производый класс может наследовать только от класса, который имеет модификатор public.
Если класс объявлен с модификатором sealed , то от этого класса нельзя наследовать и создавать производные классы. Например, следующий класс не допускает создание наследников:
Нельзя унаследовать класс от статического класса.
Доступ к членам базового класса из класса-наследника
Вернемся к нашим классам Person и Employee. Хотя Employee наследует весь функционал от класса Person, посмотрим, что будет в следующем случае:
Этот код не сработает и выдаст ошибку, так как переменная _name объявлена с модификатором private и поэтому к ней доступ имеет только класс Person . Но зато в классе Person определено общедоступное свойство Name, которое мы можем использовать, поэтому следующий код у нас будет работать нормально:
Таким образом, производный класс может иметь доступ только к тем членам базового класса, которые определены с модификаторами private protected (если базовый и производный класс находятся в одной сборке), public , internal (если базовый и производный класс находятся в одной сборке), protected и protected internal .
Ключевое слово base
Теперь добавим в наши классы конструкторы:
Класс Person имеет конструктор, который устанавливает свойство Name. Поскольку класс Employee наследует и устанавливает то же свойство Name, то логично было бы не писать по сто раз код установки, а как-то вызвать соответствующий код класса Person. К тому же свойств, которые надо установить в конструкторе базового класса, и параметров может быть гораздо больше.
С помощью ключевого слова base мы можем обратиться к базовому классу. В нашем случае в конструкторе класса Employee нам надо установить имя и компанию. Но имя мы передаем на установку в конструктор базового класса, то есть в конструктор класса Person, с помощью выражения base(name) .
Конструкторы в производных классах
Конструкторы не передаются производному классу при наследовании. И если в базовом классе не определен конструктор по умолчанию без параметров, а только конструкторы с параметрами (как в случае с базовым классом Person), то в производном классе мы обязательно должны вызвать один из этих конструкторов через ключевое слово base. Например, из класса Employee уберем определение конструктора:
В данном случае мы получим ошибку, так как класс Employee не соответствует классу Person, а именно не вызывает конструктор базового класса. Даже если бы мы добавили какой-нибудь конструктор, который бы устанавливал все те же свойства, то мы все равно бы получили ошибку:
То есть в классе Employee через ключевое слово base надо явным образом вызвать конструктор класса Person:
Либо в качестве альтернативы мы могли бы определить в базовом классе конструктор без параметров:
Тогда в любом конструкторе производного класса, где нет обращения конструктору базового класса, все равно неявно вызывался бы этот конструктор по умолчанию. Например, следующий конструктор
Фактически был бы эквивалентен следующему конструктору:
Порядок вызова конструкторов
При вызове конструктора класса сначала отрабатывают конструкторы базовых классов и только затем конструкторы производных. Например, возьмем следующие классы:
При создании объекта Employee:
Мы получим следующий консольный вывод:
В итоге мы получаем следующую цепь выполнений.
Вначале вызывается конструктор Employee(string name, int age, string company) . Он делегирует выполнение конструктору Person(string name, int age)
Вызывается конструктор Person(string name, int age) , который сам пока не выполняется и передает выполнение конструктору Person(string name)
Вызывается конструктор Person(string name) , который передает выполнение конструктору класса System.Object, так как это базовый по умолчанию класс для Person.
Выполняется конструктор System.Object.Object() , затем выполнение возвращается конструктору Person(string name)
Выполняется тело конструктора Person(string name) , затем выполнение возвращается конструктору Person(string name, int age)
Выполняется тело конструктора Person(string name, int age) , затем выполнение возвращается конструктору Employee(string name, int age, string company)
Выполняется тело конструктора Employee(string name, int age, string company) . В итоге создается объект Employee
Одним из ключевых аспектов объектно-ориентированного программирования является наследование. С помощью наследования можно расширить функционал уже имеющихся классов за счет добавления нового функционала или изменения старого. Например, имеется следующий класс Person, описывающий отдельного человека:
И, возможно, впоследствии мы захотим добавить еще один класс, который описывает сотрудника предприятия - класс Employee. Так как этот класс реализует тот же функционал, что и класс Person, поскольку сотрудник - это также и человек, то было бы рационально сделать класс Employee производным (наследником, подклассом) от класса Person, который, в свою очередь, называется базовым классом, родителем или суперклассом:
Чтобы объявить один класс наследником от другого, надо использовать после имени класса-наследника ключевое слово extends , после которого идет имя базового класса. Для класса Employee базовым является Person, и поэтому класс Employee наследует все те же поля и методы, которые есть в классе Person.
Если в базовом классе определены конструкторы, то в конструкторе производного классы необходимо вызвать один из конструкторов базового класса с помощью ключевого слова super . Например, класс Person имеет конструктор, который принимает один параметр. Поэтому в классе Employee в конструкторе нужно вызвать конструктор класса Person. То есть вызов super(name) будет представлять вызов конструктора класса Person.
При вызове конструктора после слова super в скобках идет перечисление передаваемых аргументов. При этом вызов конструктора базового класса должен идти в самом начале в конструкторе производного класса. Таким образом, установка имени сотрудника делегируется конструктору базового класса.
Причем даже если производный класс никакой другой работы не производит в конструкторе, как в примере выше, все равно необходимо вызвать конструктор базового класса.
Производный класс имеет доступ ко всем методам и полям базового класса (даже если базовый класс находится в другом пакете) кроме тех, которые определены с модификатором private . При этом производный класс также может добавлять свои поля и методы:
В данном случае класс Employee добавляет поле company, которое хранит место работы сотрудника, а также метод work.
Переопределение методов
Производный класс может определять свои методы, а может переопределять методы, которые унаследованы от базового класса. Например, переопределим в классе Employee метод display:
Перед переопределяемым методом указывается аннотация @Override . Данная аннотация в принципе необязательна.
При переопределении метода он должен иметь уровень доступа не меньше, чем уровень доступа в базовом класса. Например, если в базовом классе метод имеет модификатор public, то и в производном классе метод должен иметь модификатор public.
Однако в данном случае мы видим, что часть метода display в Employee повторяет действия из метода display базового класса. Поэтому мы можем сократить класс Employee:
С помощью ключевого слова super мы также можем обратиться к реализации методов базового класса.
Запрет наследования
Хотя наследование очень интересный и эффективный механизм, но в некоторых ситуациях его применение может быть нежелательным. И в этом случае можно запретить наследование с помощью ключевого слова final . Например:
Если бы класс Person был бы определен таким образом, то следующий код был бы ошибочным и не сработал, так как мы тем самым запретили наследование:
Кроме запрета наследования можно также запретить переопределение отдельных методов. Например, в примере выше переопределен метод display() , запретим его переопределение:
В этом случае класс Employee не сможет переопределить метод display.
Динамическая диспетчеризация методов
Наследование и возможность переопределения методов открывают нам большие возможности. Прежде всего мы можем передать переменной суперкласса ссылку на объект подкласса:
Так как Employee наследуется от Person, то объект Employee является в то же время и объектом Person. Грубо говоря, любой работник предприятия одновременно является человеком.
Однако несмотря на то, что переменная представляет объект Person, виртуальная машина видит, что в реальности она указывает на объект Employee. Поэтому при вызове методов у этого объекта будет вызываться та версия метода, которая определена в классе Employee, а не в Person. Например:
Консольный вывод данной программы:
При вызове переопределенного метода виртуальная машина динамически находит и вызывает именно ту версию метода, которая определена в подклассе. Данный процесс еще называется dynamic method lookup или динамический поиск метода или динамическая диспетчеризация методов.
Привет! В прошлый раз мы говорили о конструкторах, и узнали о них достаточно много. Сейчас мы поговорим о такой вещи, как конструкторы базовых классов. Что такое базовый класс ? Дело в том, что в Java несколько разных классов могут иметь общее происхождение. Это называется наследованием . У нескольких классов-потомков может быть один общий класс-предок. Например, представим что у нас есть класс Animal (животное): Мы можем создать для него, например, 2 класса-потомка — Cat и Dog . Это делается с использованием ключевого слова extends . Это может нам пригодиться в будущем. Например, если будет задача ловить мышей — создадим в программе объект Cat . Если задача бегать за палочкой — тут мы используем объект Dog . А если будем создавать программу, симулирующую ветеринарную клинику — она будет работать с классом Animal (чтобы уметь лечить и кошек, и собак). Очень важно запомнить на будущее, что при создании объекта в первую очередь вызывается конструктор его базового класса , а только потом — конструктор самого класса, объект которого мы создаем. То есть при создании объекта Cat сначала отработает конструктор класса Animal , а только потом конструктор Cat . Чтобы убедиться в этом — добавим в конструкторы Cat и Animal вывод в консоль. Вывод в консоль: Действительно, все так и работает! Для чего это нужно? Например, чтобы не дублировать общие поля двух классов. Например, у каждого животного есть сердце и мозг, но не у каждого есть хвост. Мы можем объявить общие для всех животных поля brain и heart в родительском классе Animal , а поле tail — в подклассе Cat . Теперь мы создадим конструктор для класса Cat , куда передадим все 3 поля. Обрати внимание: конструктор успешно работает, хотя в классе Cat нет полей brain и heart . Эти поля “подтянулись” из базового класса Animal . У класса-наследника есть доступ к полям базового класса, поэтому в нашем классе Cat они видны. Поэтому нам не нужно в классе Cat дублировать эти поля — мы можем взять их из класса Animal . Более того, мы можем явно вызвать конструктор базового класса в конструкторе класса-потомка. Базовый класс еще называют “ суперклассом ”, поэтому в Java для его обозначения используется ключевое слово super . В предыдущем примере Мы отдельно присваивали каждое поле, которое есть в нашем родительском классе. На самом деле этого можно не делать. Достаточно вызвать конструктор родительского класса и передать ему нужные параметры: В конструкторе Cat мы вызвали конструктор Animal и передали в него два поля. Нам осталось явно проинициализировать только одно поле — tail , которого в Animal нет. Помнишь, мы говорили о том, что при создании объекта в первую очередь вызывается конструктор класса-родителя? Так вот, именно поэтому слово super() всегда должно стоять в конструкторе первым! Иначе логика работы конструкторов будет нарушена и программа выдаст ошибку. Компилятор знает, что при создании объекта класса-потомка сначала вызывается конструктор базового класса. И если ты попытаешься вручную изменить это поведение - он не позволит этого сделать.
Процесс создания объекта.
Инициализируются статические переменные базового класса ( Animal ). В нашем случае — переменной animalCount класса Animal присваивается значение 7700000.
Инициализируются статические переменные класса-потомка ( Cat ). Обрати внимание — мы все еще внутри конструктора Animal , а в консоли уже написано:
Дальше инициализируются нестатические переменные базового класса . Мы специально присвоили им первоначальные значения, которые потом в конструкторе меняются на новые. Конструктор Animal еще не отработал до конца, но первоначальные значения brain и heart уже присвоены:
Начинает работу конструктор базового класса .
В том, что этот этап идет только четвертым по счету, мы уже убедились: в первых трех пунктах на момент начала работы конструктора Animal многим переменным уже присвоены значения.
Читайте также: