Что такое наследование
Это принцип создание класса на базе уже существующего, при этом у нас есть возможность пользоваться функционалом (свойствами и методами) базового. Классы созданные таким образом называются производными или дочерними, а на базе которого создаются — родителем или базовым.
Этот механизм в объектно ориентированном программировании очень сильная фича. Она в несколько раз экономит время на создание проекта, а также не нагружает его повторяющимся кодом.
Производный класс мы можем усовершенствовать, добавляя:
- Новые переменные.
- Функции.
- Конструкторы.
И все это не изменяя базовый класс.
Например, на базе класса про животного можно создать потомка про собаку.
Порядок создания
Конструктор выполняет свою работу в следующем порядке.
-
Вызывает конструкторы базовых классов и членов в порядке объявления.
-
Если класс является производным от виртуальных базовых классов, конструктор инициализирует указатели виртуальных базовых классов объекта.
-
Если класс имеет или наследует виртуальные функции, конструктор инициализирует указатели виртуальных функций объекта. Указатели виртуальных функций указывают на таблицу виртуальных функций класса, чтобы обеспечить правильную привязку вызовов виртуальных функций к коду.
-
Выполняет весь код в теле функции.
В следующем примере показан порядок, в котором конструкторы базовых классов и членов вызываются в конструкторе для производного класса. Сначала вызывается конструктор базового класса, затем инициализируются члены базового класса в порядке их появления в объявлении класса. После этого вызывается конструктор производного класса.
Выходные данные будут выглядеть следующим образом.
Конструктор производного класса всегда вызывает конструктор базового класса, чтобы перед выполнением любых дополнительных операций иметь в своем распоряжении полностью созданные базовые классы. Конструкторы базовых классов вызываются в порядке наследования — например, если является производным от класса , производного от, то сначала вызывается конструктор, затем конструктор, а затем конструктор.
Если базовый класс не имеет конструктор по умолчанию, в конструкторе производного класса необходимо указать параметры конструктора базового класса.
Если конструктор создает исключение, то удаление выполняется в порядке, обратном созданию.
-
Отменяется код в теле функции конструктора.
-
Объекты базовых классов и объекты-члены удаляются в порядке, обратном объявлению.
-
Если конструктор не является делегирующим, удаляются все полностью созданные объекты базовых классов и объекты-члены. Однако поскольку сам объект создан не полностью, деструктор не выполняется.
Абстрактные методы и классы
Если класс объявлен с ключевым словом
abstract , то он называется абстрактным классом. Он может иметь, а может и не иметь абстрактных методов.
Monster.java
Java
abstract class Monster {
}
1 |
abstractclassMonster{ } |
Абстрактным методом называется метод, объявленный с ключевым словом
abstract и не имеющий тела метода.
Java
abstract void myAbstractMethod(int myParam1, double myParam2);
1 | abstractvoidmyAbstractMethod(intmyParam1,doublemyParam2); |
Если в классе есть абстрактные методы, то он ДОЛЖЕН быть объявлен абстрактным.
Нельзя создать экземпляр абстрактного класса, но можно указать абстрактный класс в качестве базового класса.
Дочерний класс от абстрактного класса должен либо дать реализацию всем его абстрактным методам, либо сам быть абстрактным классом.
Абстрактные классы очень похожи на интерфейсы, но абстрактные классы могут иметь поля экземпляров и могут иметь методы с модификатором доступа отличным от
public. Также вы можете наследовать свой класс только от одного другого класса, но вы можете реализовать любое количество интерфейсов.
Выбрать между абстрактным классом и интерфейсом бывает довольно сложно. Старайтесь руководствоваться правилами, описанными ниже.
Используйте абстрактные классы, если:
- Вы хотите использовать общий код в нескольких близко связанных классах.
- Вы ожидаете, что классы, которые будут наследоваться от вашего абстрактного класса, имеют большое количество общих полей или требуют использования модификаторов доступа отличных от
public. - Вам нужно объявить поля экземпляров или класса, а не только константы.
Используйте интерфейсы в следующих ситуациях:
Вы ожидаете, что интерфейс будут реализовывать не связанные друг с другом классы
Интерфейсы
Comparable и
Cloneable , например, реализует очень большое количество совершенно разных классов.
Вы хотите указать поведение определённого типа, но вам абсолютно не важно, кто будет реализовывать это поведение.
Вам нужно множественное наследование типов.. Для примера абстрактного класса представьте ситуацию, что вам нужно реализовать несколько различных видов монстров:
Goblin ,
Hobgoblin ,
Orc ,
Gremlin и
Genie
Каждый из эти монстров имеет свои различные особенности, которые будут реализовываться в соответствующем классе, но все эти монстры будут уметь ходить и иметь координаты в пространстве, и у каждого из них будет уровень здоровья. В этом случае можно заложить умение ходить, координаты и уровень здоровья в базовом классе
Monster, который сделать абстрактным, и в котором объявить абстрактные методы для управления повадками и прочими вещами, реализации которых будут в соответствующих дочерних классах
Для примера абстрактного класса представьте ситуацию, что вам нужно реализовать несколько различных видов монстров:
Goblin ,
Hobgoblin ,
Orc ,
Gremlin и
Genie. Каждый из эти монстров имеет свои различные особенности, которые будут реализовываться в соответствующем классе, но все эти монстры будут уметь ходить и иметь координаты в пространстве, и у каждого из них будет уровень здоровья. В этом случае можно заложить умение ходить, координаты и уровень здоровья в базовом классе
Monster, который сделать абстрактным, и в котором объявить абстрактные методы для управления повадками и прочими вещами, реализации которых будут в соответствующих дочерних классах.
Цикл статей «Учебник Java 8».
Следующая статья — «Java 8 перечисления».
Предыдущая статья — «Java 8 интерфейсы».
Наследование[]
Наследование — один из четырёх важнейших механизмов объектно-ориентированного программирования (наряду с инкапсуляцией, полиморфизмом и абстракцией), позволяющий описать новый класс на основе уже существующего (родительского), при этом свойства и функциональность родительского класса заимствуются новым классом.
Другими словами, класс-наследник реализует спецификацию уже существующего класса (базовый класс). Это позволяет обращаться с объектами класса-наследника точно так же, как с объектами базового класса.
Простое наследование:
Класс, от которого произошло наследование, называется базовым или родительским (англ. base class). Классы, которые произошли от базового, называются потомками, наследниками или производными классами (англ. derived class).
В некоторых языках используются абстрактные классы. Абстрактный класс — это класс, содержащий хотя бы один абстрактный метод, он описан в программе, имеет поля, методы и не может использоваться для непосредственного создания объекта. То есть от абстрактного класса можно только наследовать. Объекты создаются только на основе производных классов, наследованных от абстрактного. Например, абстрактным классом может быть базовый класс «сотрудник вуза», от которого наследуются классы «аспирант», «профессор» и т. д. Так как производные классы имеют общие поля и функции (например, поле «год рождения»), то эти члены класса могут быть описаны в базовом классе. В программе создаются объекты на основе классов «аспирант», «профессор», но нет смысла создавать объект на основе класса «сотрудник вуза».
Множественное наследование
При множественном наследовании у класса может быть более одного предка. В этом случае класс наследует методы всех предков. Достоинства такого подхода в большей гибкости. Множественное наследование реализовано в C++. Из других языков, предоставляющих эту возможность, можно отметить Python и Эйфель. Множественное наследование поддерживается в языке UML.
Множественное наследование — потенциальный источник ошибок, которые могут возникнуть из-за наличия одинаковых имен методов в предках. В языках, которые позиционируются как наследники C++ (Java, C# и др.), от множественного наследования было решено отказаться в пользу интерфейсов. Практически всегда можно обойтись без использования данного механизма. Однако, если такая необходимость все-таки возникла, то, для разрешения конфликтов использования наследованных методов с одинаковыми именами, возможно, например, применить операцию расширения видимости — «::» — для вызова конкретного метода конкретного родителя.
Попытка решения проблемы наличия одинаковых имен методов в предках была предпринята в языке Эйфель, в котором при описании нового класса необходимо явно указывать импортируемые члены каждого из наследуемых классов и их именование в дочернем классе.
Большинство современных объектно-ориентированных языков программирования (C#, Java, Delphi и др.) поддерживают возможность одновременно наследоваться от класса-предка и реализовать методы нескольких интерфейсов одним и тем же классом. Этот механизм позволяет во многом заменить множественное наследование — методы интерфейсов необходимо переопределять явно, что исключает ошибки при наследовании функциональности одинаковых методов различных классов-предков.
Классовое наследование
Классовое наследование в JavaScript можно рассматривать как функциональное наследование с расширенными возможностями: создание класса с использованием ключевого слова class фактически является формой функции-конструктора, поэтому классовое наследование может как использовать ключевое слово class из ES6, так и не использовать его (функциональное наследование через функции-конструкторы).
Особенности классового наследования:
- для создания дочернего класса в объявлении класса (или в выражениях класса) применяется ключевое слово extends, использующее прототипы (prototype);
- для вызова функций, принадлежащих родителю объекта, используется ключевое слово super.
Ключевое слово extends позволяет наследовать методы родительского класса, вынесенные в prototype:
// Класс Person содержит только свойства
class Person {
constructor(first, last, age, gender) {
this.name = { first, last };
this.age = age;
}
}
// Методы класса Person вынесены в прототип
Person.prototype.greeting = function () {
return «Hi! I’m » + this.name.first + «.»;
};
// Создадим наследник класса Person
class Teacher extends Person {}
let teacher = new Teacher(«Serg»,’VAN’);
console.log(teacher); // Teacher {name: {first: «Serg», last: «VAN»}, age: undefined}
console.log(teacher.__proto__); // Person {constructor: ƒ}
console.log(teacher.greeting()); // Hi! I’m Serg. (метод родительского класса доступен)
1 |
// Класс Person содержит только свойства classPerson{ constructor(first,last,age,gender){ this.name={first,last}; this.age=age; } } Person.prototype.greeting=function(){ return»Hi! I’m «+this.name.first+».»; }; classTeacherextendsPerson{} let teacher=newTeacher(«Serg»,’VAN’); console.log(teacher);// Teacher {name: {first: «Serg», last: «VAN»}, age: undefined} console.log(teacher.__proto__);// Person {constructor: ƒ} console.log(teacher.greeting());// Hi! I’m Serg. (метод родительского класса доступен) |
Ключевое слово super используется для вызова функций, принадлежащих родителю объекта. В конструкторе ключевое слово super() используется как функция, вызывающая родительский конструктор: её необходимо вызвать до первого обращения к ключевому слову this в теле конструктора:
class Polygon {
constructor(height, width) {
this.name = «Polygon»;
this.height = height;
this.width = width;
}
}
class Square extends Polygon { // ключевое слово extends использовано для создания дочернего класса
constructor(height, width) {
// this.height; // ReferenceError, super должен быть вызван первым
super(height, width); // Вызов метода конструктора родительского класса с длинами, указанными для ширины и высоты класса Polygon
this.name = «Square»;
}
get area() {
return this.height * this.width;
}
}
let s = new Square(300,200);
console.log(s); // Square {name: «Square», height: 300, width: 200}
console.log(s.area); // 60000
1 |
classPolygon{ constructor(height,width){ this.name=»Polygon»; this.height=height; this.width=width; } } classSquareextendsPolygon{// ключевое слово extends использовано для создания дочернего класса constructor(height,width){ // this.height; // ReferenceError, super должен быть вызван первым super(height,width);// Вызов метода конструктора родительского класса с длинами, указанными для ширины и высоты класса Polygon this.name=»Square»; } get area(){ returnthis.height *this.width; } } lets=newSquare(300,200); console.log(s);// Square {name: «Square», height: 300, width: 200} console.log(s.area);// 60000 |
Преимущества и ограничения унаследованных разрешений
Существует два основных преимущества использования унаследованных разрешений для управления тем, кто из пользователей может просматривать папки в пространстве имен DFS:
- Можно быстро применить унаследованные разрешения к множеству папок, не прибегая к использованию сценариев.
- Можно применять унаследованные разрешения к корням пространства имен и папкам без конечных объектов.
Несмотря на преимущества, с унаследованными разрешениями в пространствах имен DFS связан ряд ограничений, которые делают их неприменимыми для большинства сред:
- Изменения унаследованных разрешений не реплицируются на другие серверы пространства имен. Таким образом, использовать унаследованные разрешения имеет смысл только применительно к изолированным пространствам имен или в средах, где можно реализовать стороннюю систему репликации для обеспечения синхронизации списков управления доступом (ACL) на всех серверах пространства имен.
- Оснастка «Управление DFS» и команда Dfsutil не позволяют просматривать или изменять унаследованные разрешения. Таким образом, для управления пространством имен в дополнение к оснастке «Управление DFS» или команде Dfsutil необходимо использовать проводник или команду Icacls.
- При использовании унаследованных разрешений невозможно изменить разрешения папки с конечными объектами, кроме как с помощью команды Dfsutil. Пространства имен DFS автоматически удаляют разрешения с папок с конечными объектами, заданные с использованием других средств или методов.
- При задании разрешений для папки с конечными объектами, когда вы используете унаследованные разрешения, список управления доступом, который вы установили для папки с конечными объектами, объединяется с разрешениями, наследуемыми от родителя папки в файловой системе. Необходимо изучить оба набора разрешений, чтобы определить, каковы будут результирующие разрешения.
Примечание
При использовании унаследованных разрешений проще всего задать разрешения для корней пространства имен и папок без конечных объектов. Затем эти унаследованные разрешения можно использовать для папок с конечными объектами, чтобы они наследовали все разрешения от своих родителей.
Цепочки наследования
Также возможно наследование от класса, который сам является производным от другого класса. В этом нет ничего примечательного или особенного – всё происходит, как в примерах выше.
Например, давайте напишем класс руководителя, . Руководитель () является сотрудником (), который является человеком (). Мы уже написали класс , поэтому давайте использовать его в качестве базового класса для наследования :
Теперь наша диаграмма наследования выглядит так:
Рисунок 5 – Диаграмма наследования
Все объекты наследуют функции и переменные как от , так и от , и добавляют свою собственную переменную-член .
Создавая такие цепочки наследования, мы можем создать набор пригодных для повторного использования классов, которые являются очень обобщенными (вверху) и постепенно становятся более конкретными на каждом уровне наследования.
Как наследовать?
Основной и дочерний класcы создают определенную иерархию. На вершине — всегда базовый, а под ним строятся подклассы. При этом преобразованный класс служит родителем для других подклассов.
В Java существуют такие способы преобразования:
- одиночное;
- многоуровневое;
- иерархическое.
Рассмотрим их по порядку.
Одиночное наследование
Механизм одиночного наследования очень прост: подкласс получает свойства только от одного основного класса:
class Pet{ public void walk (){System.out.println("I will walk");} class Cow extends Pet{ public void moo (){System.out.println("I will moo");} }
Результат:
I will walk I will moo
Многоуровневое наследование
Многоуровневый процесс происходит при наследовании дополнительного класса от базового, затем этот же класс действует уже как основной для следующего:
class Pet{ public void walk (){System.out.println("I will walk.");} } class Cat extends Pet{ public void sleep (){System.out.println("I will sleep ");} } class Kitty extends Cat{ public void purr (){System.out.println("I will purr");} } class TestIn { public static void main(String args[]){ Kitty c=new Kitty(); c.walk (); c.sleep(); c.purr(); }}
Результат:
I will walk I will sleep I will purr
То есть, в конце мы обращаемся к методу walk() от Pet, потом к sleep() от Cat, а затем к purr () от Kitty.
Иерархическое наследование
Иерархическое наследование происходит, когда несколько подклассов получают разрешение от одного суперкласса:
class Pet{ public void walk(){ System.out.println ("I will walk");} } class Cat extends Pet{ public void sleep (){ System.out.println("I will sleep");} } class Kitty extends Pet{ public void purr(){ System.out.println("I will purr");} } class TestIn { public static void main(String args[]){ Kitty c=new Kitty(); c.purr(); c.walk(); //c.sleep -Error }}
Результат:
I will purr I will walk
Класс Kitty унаследовал от Pet walk(), а также у него есть свой purr (), соответственно метод sleep() от Cat ему не доступен.
Запрет наследования
Посмотрим на проблему и с другой стороны. Представьте, что вы год назад написали класс, который находится в середине иерархии классов. Отладили его, провели тесты и с ним все в порядке. Сейчас вы пишете другой класс, при этом у вас, как обычно, что-то не получается и сроки поджимают.
В этот момент к вам подходит коллега и говорит:
— Слушай, я унаследовал твой класс и класс Угрюмова. И получил алмаз смерти. Иди и решай с Угрюмовым проблему неоднозначности.
Как вы понимаете ответить этому программисту хочется только одно:
— А может ты не будешь наследовать мой класс?!
Для подобных случаев в стандарт C++ 11 добавлен спецификатор final, который запрещает наследовать данный класс. Для этого нужно написать ключевое слово final после имени класса.
class Logo final: Rectangle, Circle
1 | classLogo finalRectangle,Circle |
После этого появление «алмаза смерти» в этой ветке наследования будет исключено, так не будет самого наследования. Компилятор не даст создать производный класс от данного класса.
Переопределение методов в C#
Чтобы переопределить метод в классе-наследнике, этот метод определяется с в классе модификатором . В отличие от перегрузки, переопределенный метод в классе-наследнике должен иметь тот же набор параметров, что и виртуальный метод в базовом классе. Например, рассмотрим следующие классы:
class Person { public string Name { get; set; } public Person(string name) { Name = name; } public virtual void Display() { Console.WriteLine(Name); } } class Employee : Person { public string Company { get; set; } public Employee(string name, string company) : base(name) { Company = company; } }
Класс представляет человека. В свою очередь, класс наследуется от и представляет сотрудника организации и, кроме унаследованного свойства , имеет еще одно свойство — . Чтобы сделать метод доступным для переопределения, этот метод определен с модификатором . Поэтому мы можем переопределить этот метод (а можем и не переопределять — всё зависит от наших потребностей).
Допустим, что нас устраивает реализация метода из базового класса. В таком случае, объекты будут использовать реализацию метода из класса :
Person p1 = new Person("Вася"); p1.Display(); // вызов метода Display из класса Person Employee e1 = new Employee("Билл", "Microsoft"); e1.Display(); // вызов метода Display из класса Person
В консоли мы увидим следующее:
Вася
Билл
Если же нас не устраивает функционал метода в родителе, то мы можем переопределить виртуальный метод. Для этого в классе-наследнике определяется метод с модификатором , который имеет то же самое имя и набор параметров:
class Employee : Person { public string Company { get; set; } public Employee(string name, string company): base(name) { Company = company; } public override void Display() { Console.WriteLine($"{Name} работает в {Company}"); } }
Пример использования:
Person p1 = new Person("Bill"); p1.Display(); // вызов метода Display из класса Person Employee p2 = new Employee("Tom", "Microsoft"); p2.Display(); // вызов метода Display из класса Employee
В консоли будут следующие строки:
Bill
Tom работает в Microsoft
Виртуальные методы базового класса определяют интерфейс всей иерархии классов. Это значит, что в любом производном классе, который не является прямым наследником от базового класса, можно переопределить виртуальные методы. Например, мы можем определить класс , который будет производным от , и в нем также переопределить метод .
При переопределении виртуальных методов в C# необходимо учитывать следующие ограничения:
- Виртуальный и переопределенный методы должны иметь один и тот же . Если виртуальный метод определен с помощью модификатора , то и переопредленный метод также должен иметь модификатор .
- Нельзя переопределить или объявить виртуальным статический метод.
Переопределение конструктора
С конструкторами немного сложнее.
До сих пор у не было своего конструктора.
Согласно , если класс расширяет другой класс и не имеет конструктора, то автоматически создаётся такой «пустой» конструктор:
Как мы видим, он просто вызывает конструктор родительского класса. Так будет происходить, пока мы не создадим собственный конструктор.
Давайте добавим конструктор для . Он будет устанавливать в дополнение к :
Упс! При создании кролика – ошибка! Что не так?
Если коротко, то в классах-потомках конструктор обязан вызывать , и (!) делать это перед использованием .
…Но почему? Что происходит? Это требование кажется довольно странным.
Конечно, всему есть объяснение. Давайте углубимся в детали, чтобы вы действительно поняли, что происходит.
В JavaScript существует различие между «функцией-конструктором наследующего класса» и всеми остальными. В наследующем классе соответствующая функция-конструктор помечена специальным внутренним свойством .
Разница в следующем:
- Когда выполняется обычный конструктор, он создаёт пустой объект и присваивает его .
- Когда запускается конструктор унаследованного класса, он этого не делает. Вместо этого он ждёт, что это сделает конструктор родительского класса.
Поэтому, если мы создаём собственный конструктор, мы должны вызвать , в противном случае объект для не будет создан, и мы получим ошибку.
Чтобы конструктор работал, он должен вызвать до того, как использовать , чтобы не было ошибки:
Начало работы
Прежде всего сделайте себе локальную копию нашего файла oojs-class-inheritance-start.html (он также работает в режиме реального времени). В файле вы найдёте тот же пример конструктора , который мы использовали на протяжении всего модуля, с небольшим отличием — мы определили внутри конструктора только лишь свойства:
Все методы определены в прототипе конструктора. Например:
Примечание. В исходном коде вы также увидите определённые методы и . Позже вы увидите, как они могут быть унаследованы другими конструкторами.
Скажем так, мы хотели создать класс , подобный тому, который мы описали в нашем первоначальном объектно-ориентированном определении, которое наследует всех членов от , но также включает в себя:
- Новое свойство, — оно будет содержать предмет, который преподаёт учитель.
- Обновлённый метод , который звучит немного более формально, чем стандартный метод — более подходит для учителя, обращающегося к некоторым ученикам в школе.