Сколько наследников может быть у класса родителя
Одним из ключевых аспектов объектно-ориентированного программирования является наследование. С помощью наследования можно расширить функционал уже имеющихся классов за счет добавления нового функционала или изменения старого. Например, имеется следующий класс Person, описывающий отдельного человека:
class Person < String name; public String getName()< return name; >public Person(String name) < this.name=name; >public void display() < System.out.println("Name: " + name); >>
И, возможно, впоследствии мы захотим добавить еще один класс, который описывает сотрудника предприятия — класс Employee. Так как этот класс реализует тот же функционал, что и класс Person, поскольку сотрудник — это также и человек, то было бы рационально сделать класс Employee производным (наследником, подклассом) от класса Person, который, в свою очередь, называется базовым классом, родителем или суперклассом:
class Employee extends Person < public Employee(String name)< super(name); // если базовый класс определяет конструктор // то производный класс должен его вызвать >>
Чтобы объявить один класс наследником от другого, надо использовать после имени класса-наследника ключевое слово extends , после которого идет имя базового класса. Для класса Employee базовым является Person, и поэтому класс Employee наследует все те же поля и методы, которые есть в классе Person.
Если в базовом классе определены конструкторы, то в конструкторе производного классы необходимо вызвать один из конструкторов базового класса с помощью ключевого слова super . Например, класс Person имеет конструктор, который принимает один параметр. Поэтому в классе Employee в конструкторе нужно вызвать конструктор класса Person. То есть вызов super(name) будет представлять вызов конструктора класса Person.
При вызове конструктора после слова super в скобках идет перечисление передаваемых аргументов. При этом вызов конструктора базового класса должен идти в самом начале в конструкторе производного класса. Таким образом, установка имени сотрудника делегируется конструктору базового класса.
Причем даже если производный класс никакой другой работы не производит в конструкторе, как в примере выше, все равно необходимо вызвать конструктор базового класса.
public class Program < public static void main(String[] args) < Person tom = new Person("Tom"); tom.display(); Employee sam = new Employee("Sam"); sam.display(); >> class Person < String name; public String getName()< return name; >public Person(String name) < this.name=name; >public void display() < System.out.println("Name: " + name); >> class Employee extends Person < public Employee(String name)< super(name); // если базовый класс определяет конструктор // то производный класс должен его вызвать >>
Производный класс имеет доступ ко всем методам и полям базового класса (даже если базовый класс находится в другом пакете) кроме тех, которые определены с модификатором private . При этом производный класс также может добавлять свои поля и методы:
public class Program < public static void main(String[] args) < Employee sam = new Employee("Sam", "Microsoft"); sam.display(); // Sam sam.work(); // Sam works in Microsoft >> class Person < String name; public String getName()< return name; >public Person(String name) < this.name=name; >public void display() < System.out.println("Name: " + name); >> class Employee extends Person < String company; public Employee(String name, String company) < super(name); this.company=company; >public void work() < System.out.printf("%s works in %s \n", getName(), company); >>
В данном случае класс Employee добавляет поле company, которое хранит место работы сотрудника, а также метод work.
Переопределение методов
Производный класс может определять свои методы, а может переопределять методы, которые унаследованы от базового класса. Например, переопределим в классе Employee метод display:
public class Program < public static void main(String[] args) < Employee sam = new Employee("Sam", "Microsoft"); sam.display(); // Sam // Works in Microsoft >> class Person < String name; public String getName()< return name; >public Person(String name) < this.name=name; >public void display() < System.out.println("Name: " + name); >> class Employee extends Person < String company; public Employee(String name, String company) < super(name); this.company=company; >@Override public void display() < System.out.printf("Name: %s \n", getName()); System.out.printf("Works in %s \n", company); >>
Перед переопределяемым методом указывается аннотация @Override . Данная аннотация в принципе необязательна.
При переопределении метода он должен иметь уровень доступа не меньше, чем уровень доступа в базовом класса. Например, если в базовом классе метод имеет модификатор public, то и в производном классе метод должен иметь модификатор public.
Однако в данном случае мы видим, что часть метода display в Employee повторяет действия из метода display базового класса. Поэтому мы можем сократить класс Employee:
class Employee extends Person < String company; public Employee(String name, String company) < super(name); this.company=company; >@Override public void display() < super.display(); System.out.printf("Works in %s \n", company); >>
С помощью ключевого слова super мы также можем обратиться к реализации методов базового класса.
Запрет наследования
Хотя наследование очень интересный и эффективный механизм, но в некоторых ситуациях его применение может быть нежелательным. И в этом случае можно запретить наследование с помощью ключевого слова final . Например:
public final class Person
Если бы класс Person был бы определен таким образом, то следующий код был бы ошибочным и не сработал, так как мы тем самым запретили наследование:
class Employee extends Person
Кроме запрета наследования можно также запретить переопределение отдельных методов. Например, в примере выше переопределен метод display() , запретим его переопределение:
public class Person < //. public final void display()< System.out.println("Имя: " + name); >>
В этом случае класс Employee не сможет переопределить метод display.
Динамическая диспетчеризация методов
Наследование и возможность переопределения методов открывают нам большие возможности. Прежде всего мы можем передать переменной суперкласса ссылку на объект подкласса:
Person sam = new Employee("Sam", "Oracle");
Так как Employee наследуется от Person, то объект Employee является в то же время и объектом Person. Грубо говоря, любой работник предприятия одновременно является человеком.
Однако несмотря на то, что переменная представляет объект Person, виртуальная машина видит, что в реальности она указывает на объект Employee. Поэтому при вызове методов у этого объекта будет вызываться та версия метода, которая определена в классе Employee, а не в Person. Например:
public class Program < public static void main(String[] args) < Person tom = new Person("Tom"); tom.display(); Person sam = new Employee("Sam", "Oracle"); sam.display(); >> class Person < String name; public String getName() < return name; >public Person(String name) < this.name=name; >public void display() < System.out.printf("Person %s \n", name); >> class Employee extends Person < String company; public Employee(String name, String company) < super(name); this.company = company; >@Override public void display() < System.out.printf("Employee %s works in %s \n", super.getName(), company); >>
Консольный вывод данной программы:
Person Tom Employee Sam works in Oracle
При вызове переопределенного метода виртуальная машина динамически находит и вызывает именно ту версию метода, которая определена в подклассе. Данный процесс еще называется dynamic method lookup или динамический поиск метода или динамическая диспетчеризация методов.
Наследование классов
Наследование классов – это способ расширения одного класса другим классом.
Таким образом, мы можем добавить новый функционал к уже существующему.
Ключевое слово «extends»
Допустим, у нас есть класс Animal :
class Animal < constructor(name) < this.speed = 0; this.name = name; >run(speed) < this.speed = speed; alert(`$бежит со скоростью $.`); > stop() < this.speed = 0; alert(`$стоит неподвижно.`); > > let animal = new Animal("Мой питомец");
Вот как мы можем представить объект animal и класс Animal графически:
…И мы хотели бы создать ещё один class Rabbit .
Поскольку кролики – это животные, класс Rabbit должен быть основан на Animal , и иметь доступ к методам животных, так чтобы кролики могли делать то, что могут делать «общие» животные.
Синтаксис для расширения другого класса следующий: class Child extends Parent .
Давайте создадим class Rabbit , который наследуется от Animal :
class Rabbit extends Animal < hide() < alert(`$прячется!`); > > let rabbit = new Rabbit("Белый кролик"); rabbit.run(5); // Белый кролик бежит со скоростью 5. rabbit.hide(); // Белый кролик прячется!
Объект класса Rabbit имеет доступ как к методам Rabbit , таким как rabbit.hide() , так и к методам Animal , таким как rabbit.run() .
Внутри ключевое слово extends работает по старой доброй механике прототипов. Оно устанавливает Rabbit.prototype.[[Prototype]] в Animal.prototype . Таким образом, если метода не оказалось в Rabbit.prototype , JavaScript берет его из Animal.prototype .
Например, чтобы найти метод rabbit.run , движок проверяет (снизу вверх на картинке):
- Объект rabbit (не имеет run ).
- Его прототип, то есть Rabbit.prototype (имеет hide , но не имеет run ).
- Его прототип, то есть (вследствие extends ) Animal.prototype , в котором, наконец, есть метод run .
Как мы помним из главы Встроенные прототипы, сам JavaScript использует наследование на прототипах для встроенных объектов. Например, Date.prototype.[[Prototype]] является Object.prototype , поэтому у дат есть универсальные методы объекта.
После extends разрешены любые выражения
Синтаксис создания класса допускает указывать после extends не только класс, но и любое выражение.
Пример вызова функции, которая генерирует родительский класс:
function f(phrase) < return class < sayHi() < alert(phrase); >>; > class User extends f("Привет") <> new User().sayHi(); // Привет
Здесь class User наследует от результата вызова f(«Привет») .
Это может быть полезно для продвинутых приёмов проектирования, где мы можем использовать функции для генерации классов в зависимости от многих условий и затем наследовать их.
Переопределение методов
Теперь давайте продвинемся дальше и переопределим метод. По умолчанию все методы, не указанные в классе Rabbit , берутся непосредственно «как есть» из класса Animal .
Но если мы укажем в Rabbit собственный метод, например stop() , то он будет использован вместо него:
class Rabbit extends Animal < stop() < // . теперь это будет использоваться для rabbit.stop() // вместо stop() из класса Animal >>
Впрочем, обычно мы не хотим полностью заменить родительский метод, а скорее хотим сделать новый на его основе, изменяя или расширяя его функциональность. Мы делаем что-то в нашем методе и вызываем родительский метод до/после или в процессе.
У классов есть ключевое слово «super» для таких случаев.
- super.method(. ) вызывает родительский метод.
- super(. ) для вызова родительского конструктора (работает только внутри нашего конструктора).
Пусть наш кролик автоматически прячется при остановке:
class Animal < constructor(name) < this.speed = 0; this.name = name; >run(speed) < this.speed = speed; alert(`$бежит со скоростью $.`); > stop() < this.speed = 0; alert(`$стоит неподвижно.`); > > class Rabbit extends Animal < hide() < alert(`$прячется!`); > stop() < super.stop(); // вызываем родительский метод stop this.hide(); // и затем hide >> let rabbit = new Rabbit("Белый кролик"); rabbit.run(5); // Белый кролик бежит со скоростью 5. rabbit.stop(); // Белый кролик стоит. Белый кролик прячется!
Теперь у класса Rabbit есть метод stop , который вызывает родительский super.stop() в процессе выполнения.
У стрелочных функций нет super
Как упоминалось в главе Повторяем стрелочные функции, стрелочные функции не имеют super .
При обращении к super стрелочной функции он берётся из внешней функции:
class Rabbit extends Animal < stop() < setTimeout(() =>super.stop(), 1000); // вызывает родительский stop после 1 секунды > >
В примере super в стрелочной функции тот же самый, что и в stop() , поэтому метод отрабатывает как и ожидается. Если бы мы указали здесь «обычную» функцию, была бы ошибка:
// Unexpected super setTimeout(function() < super.stop() >, 1000);
Переопределение конструктора
С конструкторами немного сложнее.
До сих пор у Rabbit не было своего конструктора.
Согласно спецификации, если класс расширяет другой класс и не имеет конструктора, то автоматически создаётся такой «пустой» конструктор:
class Rabbit extends Animal < // генерируется для классов-потомков, у которых нет своего конструктора constructor(. args) < super(. args); >>
Как мы видим, он просто вызывает конструктор родительского класса. Так будет происходить, пока мы не создадим собственный конструктор.
Давайте добавим конструктор для Rabbit . Он будет устанавливать earLength в дополнение к name :
class Animal < constructor(name) < this.speed = 0; this.name = name; >// . > class Rabbit extends Animal < constructor(name, earLength) < this.speed = 0; this.name = name; this.earLength = earLength; >// . > // Не работает! let rabbit = new Rabbit("Белый кролик", 10); // Error: this is not defined.
Упс! При создании кролика – ошибка! Что не так?
Если коротко, то:
- Конструкторы в наследуемых классах должны обязательно вызывать super(. ) , и (!) делать это перед использованием this ..
…Но почему? Что происходит? Это требование кажется довольно странным.
Конечно, всему есть своё объяснение. Давайте углубимся в детали, чтобы вы действительно поняли, что происходит.
В JavaScript существует различие между «функцией-конструктором наследующего класса» и всеми остальными. В наследующем классе соответствующая функция-конструктор помечена специальным внутренним свойством [[ConstructorKind]]:»derived» .
Разница в следующем:
- Когда выполняется обычный конструктор, он создаёт пустой объект и присваивает его this .
- Когда запускается конструктор унаследованного класса, он этого не делает. Вместо этого он ждёт, что это сделает конструктор родительского класса.
Поэтому, если мы создаём собственный конструктор, мы должны вызвать super , в противном случае объект для this не будет создан, и мы получим ошибку.
Чтобы конструктор Rabbit работал, он должен вызвать super() до того, как использовать this , чтобы не было ошибки:
class Animal < constructor(name) < this.speed = 0; this.name = name; >// . > class Rabbit extends Animal < constructor(name, earLength) < super(name); this.earLength = earLength; >// . > // теперь работает let rabbit = new Rabbit("Белый кролик", 10); alert(rabbit.name); // Белый кролик alert(rabbit.earLength); // 10
Переопределение полей класса: тонкое замечание
Продвинутое замечание
В этом подразделе предполагается, что у вас уже есть определённый опыт работы с классами, возможно, в других языках программирования.
Это даёт лучшее представление о языке, а также объясняет поведение, которое может быть источником ошибок (но не очень часто).
Если вы считаете этот материал слишком трудным для понимания, просто продолжайте читать дальше, а затем вернитесь к нему через некоторое время.
Мы можем переопределять не только методы, но и поля класса.
Однако, когда мы получаем доступ к переопределенному полю в родительском конструкторе, это поведение отличается от большинства других языков программирования.
Рассмотрим этот пример:
class Animal < name = 'animal'; constructor() < alert(this.name); // (*) >> class Rabbit extends Animal < name = 'rabbit'; >new Animal(); // animal new Rabbit(); // animal
Здесь, класс Rabbit расширяет Animal и переопределяет поле name своим собственным значением.
В Rabbit нет собственного конструктора, поэтому вызывается конструктор Animal .
Что интересно, в обоих случаях: new Animal() и new Rabbit() , alert в строке (*) показывает animal .
Другими словами, родительский конструктор всегда использует своё собственное значение поля, а не переопределённое.
Что же в этом странного?
Если это ещё не ясно, сравните с методами.
Вот тот же код, но вместо поля this.name , мы вызываем метод this.showName() :
class Animal < showName() < // вместо this.name = 'animal' alert('animal'); >constructor() < this.showName(); // вместо alert(this.name); >> class Rabbit extends Animal < showName() < alert('rabbit'); >> new Animal(); // animal new Rabbit(); // rabbit
Обратите внимание: теперь результат другой.
И это то, чего мы, естественно, ожидаем. Когда родительский конструктор вызывается в производном классе, он использует переопределённый метод.
…Но для полей класса это не так. Как уже было сказано, родительский конструктор всегда использует родительское поле.
Почему же наблюдается разница?
Что ж, причина заключается в порядке инициализации полей. Поле класса инициализируется:
- Перед конструктором для базового класса (который ничего не расширяет),
- Сразу после super() для производного класса.
В нашем случае Rabbit – это производный класс. В нем нет конструктора constructor() . Как было сказано ранее, это то же самое, как если бы был пустой конструктор, содержащий только super(. args) .
Итак, new Rabbit() вызывает super() , таким образом, выполняя родительский конструктор, и (согласно правилу для производных классов) только после этого инициализируются поля его класса. На момент выполнения родительского конструктора ещё нет полей класса Rabbit , поэтому используются поля Animal .
Это тонкое различие между полями и методами характерно для JavaScript.
К счастью, такое поведение проявляется только в том случае, когда переопределенное поле используется в родительском конструкторе. Тогда может быть трудно понять, что происходит, поэтому мы объясняем это здесь.
Если это становится проблемой, её можно решить, используя методы или геттеры/сеттеры вместо полей.
Устройство super, [[HomeObject]]
Продвинутая информация
Если вы читаете учебник первый раз – эту секцию можно пропустить.
Она рассказывает о внутреннем устройстве наследования и вызов super .
Давайте заглянем «под капот» super . Здесь есть некоторые интересные моменты.
Вообще, исходя из наших знаний до этого момента, super вообще не может работать!
Ну правда, давайте спросим себя – как он должен работать, чисто технически? Когда метод объекта выполняется, он получает текущий объект как this . Если мы вызываем super.method() , то движку необходимо получить method из прототипа текущего объекта. И как ему это сделать?
Задача может показаться простой, но это не так. Движок знает текущий this и мог бы попытаться получить родительский метод как this.__proto__.method . Однако, увы, такой «наивный» путь не работает.
Продемонстрируем проблему. Без классов, используя простые объекты для наглядности.
Вы можете пропустить эту часть и перейти ниже к подсекции [[HomeObject]] , если не хотите знать детали. Вреда не будет. Или читайте далее, если хотите разобраться.
В примере ниже rabbit.__proto__ = animal . Попробуем в rabbit.eat() вызвать animal.eat() , используя this.__proto__ :
let animal = < name: "Animal", eat() < alert(`$ест.`); > >; let rabbit = < __proto__: animal, name: "Кролик", eat() < // вот как предположительно может работать super.eat() this.__proto__.eat.call(this); // (*) >>; rabbit.eat(); // Кролик ест.
В строке (*) мы берём eat из прототипа ( animal ) и вызываем его в контексте текущего объекта. Обратите внимание, что .call(this) здесь неспроста: простой вызов this.__proto__.eat() будет выполнять родительский eat в контексте прототипа, а не текущего объекта.
Приведённый выше код работает так, как задумано: выполняется нужный alert .
Теперь давайте добавим ещё один объект в цепочку наследования и увидим, как все сломается:
let animal = < name: "Животное", eat() < alert(`$ест.`); > >; let rabbit = < __proto__: animal, eat() < // . делаем что-то специфичное для кролика и вызываем родительский (animal) метод this.__proto__.eat.call(this); // (*) >>; let longEar = < __proto__: rabbit, eat() < // . делаем что-то, связанное с длинными ушами, и вызываем родительский (rabbit) метод this.__proto__.eat.call(this); // (**) >>; longEar.eat(); // Error: Maximum call stack size exceeded
Теперь код не работает! Ошибка возникает при попытке вызова longEar.eat() .
На первый взгляд все не так очевидно, но если мы проследим вызов longEar.eat() , то сможем понять причину ошибки. В обеих строках (*) и (**) значение this – это текущий объект ( longEar ). Это важно: для всех методов объекта this указывает на текущий объект, а не на прототип или что-то ещё.
Итак, в обеих линиях (*) и (**) значение this.__proto__ одно и то же: rabbit . В обоих случаях метод rabbit.eat вызывается в бесконечном цикле не поднимаясь по цепочке вызовов.
Картина того, что происходит:
- Внутри longEar.eat() строка (**) вызывает rabbit.eat со значением this=longEar .
// внутри longEar.eat() у нас this = longEar this.__proto__.eat.call(this) // (**) // становится longEar.__proto__.eat.call(this) // то же что и rabbit.eat.call(this);
// внутри rabbit.eat() у нас также this = longEar this.__proto__.eat.call(this) // (*) // становится longEar.__proto__.eat.call(this) // или (снова) rabbit.eat.call(this);
Проблема не может быть решена с помощью одного только this .
[[HomeObject]]
Для решения этой проблемы в JavaScript было добавлено специальное внутреннее свойство для функций: [[HomeObject]] .
Когда функция объявлена как метод внутри класса или объекта, её свойство [[HomeObject]] становится равно этому объекту.
Затем super использует его, чтобы получить прототип родителя и его методы.
Давайте посмотрим, как это работает – опять же, используя простые объекты:
let animal = < name: "Животное", eat() < // animal.eat.[[HomeObject]] == animal alert(`$ест.`); > >; let rabbit = < __proto__: animal, name: "Кролик", eat() < // rabbit.eat.[[HomeObject]] == rabbit super.eat(); >>; let longEar = < __proto__: rabbit, name: "Длинноух", eat() < // longEar.eat.[[HomeObject]] == longEar super.eat(); >>; // работает верно longEar.eat(); // Длинноух ест.
Это работает как задумано благодаря [[HomeObject]] . Метод, такой как longEar.eat , знает свой [[HomeObject]] и получает метод родителя из его прототипа. Вообще без использования this .
Методы не «свободны»
До этого мы неоднократно видели, что функции в JavaScript «свободны», не привязаны к объектам. Их можно копировать между объектами и вызывать с любым this .
Но само существование [[HomeObject]] нарушает этот принцип, так как методы запоминают свои объекты. [[HomeObject]] нельзя изменить, эта связь – навсегда.
Единственное место в языке, где используется [[HomeObject]] – это super . Поэтому если метод не использует super , то мы все ещё можем считать его свободным и копировать между объектами. А вот если super в коде есть, то возможны побочные эффекты.
Вот пример неверного результата super после копирования:
let animal = < sayHi() < alert("Я животное"); >>; // rabbit наследует от animal let rabbit = < __proto__: animal, sayHi() < super.sayHi(); >>; let plant = < sayHi() < alert("Я растение"); >>; // tree наследует от plant let tree = < __proto__: plant, sayHi: rabbit.sayHi // (*) >; tree.sayHi(); // Я животное (. )
Вызов tree.sayHi() показывает «Я животное». Определённо неверно.
- В строке (*) , метод tree.sayHi скопирован из rabbit . Возможно, мы хотели избежать дублирования кода?
- Его [[HomeObject]] – это rabbit , ведь он был создан в rabbit . Свойство [[HomeObject]] никогда не меняется.
- В коде tree.sayHi() есть вызов super.sayHi() . Он идёт вверх от rabbit и берёт метод из animal .
Вот диаграмма происходящего:
Методы, а не свойства-функции
Свойство [[HomeObject]] определено для методов как классов, так и обычных объектов. Но для объектов методы должны быть объявлены именно как method() , а не «method: function()» .
Для нас различий нет, но они есть для JavaScript.
В приведённом ниже примере используется синтаксис не метода, свойства-функции. Поэтому у него нет [[HomeObject]] , и наследование не работает:
let animal = < eat: function() < // намеренно пишем так, а не eat() < . // . >>; let rabbit = < __proto__: animal, eat: function() < super.eat(); >>; rabbit.eat(); // Ошибка вызова super (потому что нет [[HomeObject]])
Итого
- Чтобы унаследовать от класса: class Child extends Parent :
- При этом Child.prototype.__proto__ будет равен Parent.prototype , так что методы будут унаследованы.
- При переопределении конструктора:
- Обязателен вызов конструктора родителя super() в конструкторе Child до обращения к this .
- При переопределении другого метода:
- Мы можем вызвать super.method() в методе Child для обращения к методу родителя Parent .
- Внутренние детали:
- Методы запоминают свой объект во внутреннем свойстве [[HomeObject]] . Благодаря этому работает super , он в его прототипе ищет родительские методы.
- Поэтому копировать метод, использующий super , между разными объектами небезопасно.
- У функций-стрелок нет своего this и super , поэтому они «прозрачно» встраиваются во внешний контекст.
Задачи
Ошибка создания экземпляра класса
важность: 5
В коде ниже класс Rabbit наследует Animal .
К сожалению, объект класса Rabbit не создаётся. Что не так? Исправьте ошибку.
class Animal < constructor(name) < this.name = name; >> class Rabbit extends Animal < constructor(name) < this.name = name; this.created = Date.now(); >> let rabbit = new Rabbit("Белый кролик"); // Error: this is not defined alert(rabbit.name);
Ошибка возникает потому, что конструктор дочернего класса должен вызывать super() .
Вот правильный код:
class Animal < constructor(name) < this.name = name; >> class Rabbit extends Animal < constructor(name) < super(name); this.created = Date.now(); >> let rabbit = new Rabbit("Белый кролик"); // ошибки нет alert(rabbit.name); // White Rabbit
Улучшенные часы
важность: 5
У нас есть класс Clock . Сейчас он выводит время каждую секунду
class Clock < constructor(< template >) < this.template = template; >render() < let date = new Date(); let hours = date.getHours(); if (hours < 10) hours = '0' + hours; let mins = date.getMinutes(); if (mins < 10) mins = '0' + mins; let secs = date.getSeconds(); if (secs < 10) secs = '0' + secs; let output = this.template .replace('h', hours) .replace('m', mins) .replace('s', secs); console.log(output); >stop() < clearInterval(this.timer); >start() < this.render(); this.timer = setInterval(() =>this.render(), 1000); > >
Создайте новый класс ExtendedClock , который будет наследоваться от Clock и добавьте параметр precision – количество миллисекунд между «тиками». Установите значение в 1000 (1 секунда) по умолчанию.
- Сохраните ваш код в файл extended-clock.js
- Не изменяйте класс clock.js . Расширьте его.
class ExtendedClock extends Clock < constructor(options) < super(options); let < precision = 1000 >= options; this.precision = precision; > start() < this.render(); this.timer = setInterval(() =>this.render(), this.precision); > >;
Наследование
Наследование – важная составляющая объектно-ориентированного программирования. Так или иначе мы уже сталкивались с ним, ведь объекты наследуют атрибуты своих классов. Однако обычно под наследованием в ООП понимается наличие классов и подклассов. Также их называют супер- или надклассами и классами, а также родительскими и дочерними классами.
Суть наследования здесь схожа с наследованием объектами от классов. Дочерние классы наследуют атрибуты родительских, а также могут переопределять атрибуты и добавлять свои.

—>
Простое наследование методов родительского класса
В качестве примера рассмотрим два класса столов. Класс Table – родительский по отношению к DeskTable (письменные столы). Независимо от своего типа все столы имеют длину, ширину и высоту. Пусть для письменных столов также важна площадь поверхности. Общее вынесем в класс, частное – в подкласс.
Наследственная связь между классами устанавливается через подкласс. При определении дочернего после его имени в скобках указывается родительский.
class Table: def __init__(self, l, w, h): self.length = l self.width = w self.height = h class DeskTable(Table): def square(self): return self.width * self.length t1 = Table(1.5, 1.8, 0.75) t2 = DeskTable(0.8, 0.6, 0.7) print(t2.square()) # вывод: 0.48
В данном случае у класса DeskTable нет своего конструктора, поэтому он наследует его от родителя. При создании объектов передавать аргументы необходимо в обоих случаях. Попытка вызова DeskTable с пустыми скобками приведет к ошибке.
С другой стороны, экземпляры надкласса Table , согласно неким родственным связям, не наследуют метод square своего подкласса.
В этом смысле терминология «родительский и дочерний класс» не совсем верна. Наследование в ООП – это скорее аналог систематизации и классификации наподобие той, что есть в живой природе. Все млекопитающие имеют четырехкамерное сердце, но только носороги – рог.
Полное переопределение метода надкласса
Рассмотрим вариант программы с «цепочкой наследования». Пусть дочерний по отношению к Table класс DeskTable в свою очередь выступит родительским по отношению к ComputerTable (компьютерные столы):
class Table: def __init__(self, l, w, h): self.length = l self.width = w self.height = h class DeskTable(Table): def square(self): return self.width * self.length class ComputerTable(DeskTable): def square(self, monitor=0.0): return self.width * self.length - monitor t3 = ComputerTable(0.8, 0.6, 0.7) print(t3.square(0.3)) # вывод: 0.18
Допустим, по задумке разработчиков рабочая поверхность компьютерного стола может вычисляться за вычетом площади, которую занимает монитор. В результате метод square в ComputerTable имеет отличия.
Определив в дочернем классе метод, одноименный методу родительского, мы тем самым переопределяем метод родительского класса. При вызове square на экземпляры ComputerTable будет вызываться метод из этого класса, а не из родительского класса DeskTable .
В то же время ComputerTable наследует конструктор класса от своей «бабушки» – класса Table .
Дополнение, оно же расширение, метода
Часто требуется не столько заменить, то есть полностью переопределить, метод родительского класса в дочернем, сколько дополнить, то есть расширить, код метода родительского класса в дочернем. В таких случаях решением является вызов метода надкласса в теле соответствующего метода подкласса. Обычно после этого в теле метода подкласса пишется дополнительный код.
Пусть имеется подкласс кухонных столов, для которых необходимо еще одно поле – количество посадочных мест. Из-за этой детали мы вынуждены в дочернем классе переопределить конструктор родительского.
class Table: def __init__(self, l, w, h): self.length = l self.width = w self.height = h class KitchenTable(Table): def __init__(self, l, w, h, p): self.length = l self.width = w self.height = h self.places = p t4 = KitchenTable(1.5, 2, 0.75, 6)
Поскольку существенная часть кода конструктора подкласса является такой же как в надклассе, правильнее будет вызвать метод другого класса, а не дублировать код:
class Table: def __init__(self, l, w, h): self.length = l self.width = w self.height = h class KitchenTable(Table): def __init__(self, l, w, h, p): Table.__init__(self, l, w, h) self.places = p t4 = KitchenTable(1.5, 2, 0.75, 6)
Здесь в теле конструктора KitchenTable мы вызываем метод __init__ через объект-класс Table , а не через объект-экземпляр. Вспомним, что в таких случаях метод вызывается как обычная функция (объект, к которому применяется метод, не передается в качестве первого аргумента). Поэтому в конструктор надкласса мы «вручную» передаем текущий экземпляр ( self ), записывая его перед остальными аргументами.
У кода выше есть небольшой недостаток. Нам ничего не мешает (при условии совпадения количества параметров) вызвать конструктор другого класса, а не только родительского, указав его имя вместо Table . Кроме того, имя надкласса может измениться, и тогда есть риск неправильных обращений к нему из дочерних классов.
В Python с целью улучшения так называемой обслуживаемости кода можно использовать встроенную в язык функцию super . Наиболее распространенным вариантом ее применения является вызов метода родительского класса из метода подкласса:
class KitchenTable(Table): def __init__(self, l, w, h, p): super().__init__(l, w, h) self.places = p
В данном случае аргумент self в скобках вызываемого родительского метода указывать явно не требуется.
Параметры со значениями по умолчанию у родительского класса
Рассмотрим случай, когда родительский класс имеет параметры со значениями по умолчанию, а дочерний – нет:
class Table: def __init__(self, l=1, w=1, h=1): self.length = l self.width = w self.height = h class KitchenTable(Table): def __init__(self, p, l, w, h): Table.__init__(self, l, w, h) self.places = p
При таком определении классов можно создать экземпляр от Table без передачи аргументов для конструктора:
t = Table()
Можем ли мы создать экземпляр от KitchenTable , передав значение только для параметра p ? Например, вот так:
k = KitchenTable(10)
Возможно ли, что p будет присвоено число 10, а l , w и h получат по единице от родительского класса? Невозможно, будет выброшено исключение по причине несоответствия количества переданных аргументов количеству требуемых конструктором:
. k = KitchenTable(10) TypeError: __init__() missing 3 required positional arguments: 'l', 'w', and 'h'
Когда создается объект от дочернего класса, сначала вызывается его конструктор, если он есть. Интерпретатор еще не знает, что в теле этого конструктора будет вызван конструктор родительского класса. Ведь это не обязательно. Значит, если все параметры дочернего конструктора не имеют значений по умолчанию, при построении объекта все значения должны передаваться.
Поэтому, если требуется допустить создание объектов от дочернего класса без передачи аргументов, придется назначить значения по умолчанию также в конструкторе дочернего класса.
class Table: def __init__(self, l=1, w=1, h=1): self.length = l self.width = w self.height = h class KitchenTable(Table): def __init__(self, l=1, w=1, h=0.7, p=4): Table.__init__(self, l, w, h) self.places = p
Параметр p , которого нет у родительского класса, мы делаем последним не просто так. Бывает, объекты разных родственных классов создаются или обрабатываются в одном цикле, то есть по одному алгоритму. При этом у них должны быть одинаковые «интерфейсы», то есть одинаковое количество передаваемых в конструктор аргументов.
Поэтому лучше, когда методы родственных классов принимают одинаковое число параметров. А если разное, то у «лишних» должны быть значения по-умолчанию, чтобы при вызове конструктора их можно было бы не передавать. Если такие параметры находятся еще и в конце, передачу аргументов для предстоящих параметров можно выполнять без ключей.
Другой вариант – отказаться от конструктора в дочернем классе, а значение для поля places устанавливать отдельным вызовом метода:
class Table: def __init__(self, l=1, w=1, h=1): self.length = l self.width = w self.height = h class KitchenTable(Table): places = 4 def set_places(self, p): self.places = p
Здесь у всех кухонных столов по-умолчанию будет 4 места. Если мы хотим изменить значение поля places , можем вызвать метод set_places . Хотя в случае Python можем сделать это напрямую, присвоив полю. При этом у экземпляра появится собственное поле places .
k = KitchenTable() k.places = 6
Поэтому метод set_places в общем-то не нужен.
В любом случае произвольное количество мест будет устанавливаться не в конструкторе, а отдельно. Если все же требуется указывать места при создании объекта, это можно сделать и в конструкторе родителя:
class Table: def __init__(self, l=1, w=1, h=1): self.length = l self.width = w self.height = h if isinstance(self, KitchenTable): p = int(input("Сколько мест: ")) self.places = p
С помощью функции isinstance проверяется, что создаваемый объект имеет тип KitchenTable . Если это так, то у него появляется поле places .
Мы не используем параметр p со значением по умолчанию в заголовке конструктора потому, что, если объектам других родственных классов он не нужен, не происходило бы путаницы и сложностей с документированием кода.
Практическая работа
Разработайте программу по следующему описанию.
В некой игре-стратегии есть солдаты и герои. У всех есть свойство, содержащее уникальный номер объекта, и свойство, в котором хранится принадлежность команде. У солдат есть метод «иду за героем», который в качестве аргумента принимает объект типа «герой». У героев есть метод увеличения собственного уровня.
В основной ветке программы создается по одному герою для каждой команды. В цикле генерируются объекты-солдаты. Их принадлежность команде определяется случайно. Солдаты разных команд добавляются в разные списки.
Измеряется длина списков солдат противоборствующих команд и выводится на экран. У героя, принадлежащего команде с более длинным списком, увеличивается уровень.
Отправьте одного из солдат первого героя следовать за ним. Выведите на экран идентификационные номера этих двух юнитов.
Курс с примерами решений практических работ:
pdf-версия
X Скрыть Наверх
Объектно-ориентированное программирование на Python
Наследование в Java: основные правила с примерами

Наследование в Java — механизм, который позволяет одному классу получить все элементы и свойства другого класса. Этот процесс также называют расширением, преобразованием или отношением «родитель-потомок».
Професійний курс від laba: Управління командою в бізнесі.
Створюйте ефективну робочу атмосферу.
Суперкласс (родитель) — основной класс, от которого принимаются все элементы.
Подкласс (расширенный, дочерний) — класс, который преобразовывается от другого класса, при этом может иметь свои элементы.
Зачем применяют наследование в Java? Суть в том, что можно образовать новые классы, используя уже имеющийся код. При этом есть возможность добавить другие переменные в дочерний класс, получив при этом новый.
Освітній курс від laba: Delivery Manager.
Ведіть проекти до успішної доставки.
Синтаксис:
class Имя-подкласса extends Имя-суперкласса < // свойства и элементы >
Ключевое слово extends означает, что создается новый класс от уже имеющегося.
class Fruit < // поля и элементы >// применяем расширение class Apple extends Fruit < // свойства и элементы Fruit // свойства и элементы Apple >
Из примера видно, что класс Apple создается из Fruit, при этом наследуются все его элементы. Так, Fruit — это класс-родитель, а Apple — класс-потомок.
Как наследовать?
Основной и дочерний класcы создают определенную иерархию. На вершине — всегда базовый, а под ним строятся подклассы. При этом преобразованный класс служит родителем для других подклассов.
В Java существуют такие способы преобразования:
- одиночное;
- многоуровневое;
- иерархическое.
Рассмотрим их по порядку.
Практичний курс від laba: Директор з продажу.
Створюйте та розвивайте успішний відділ продажів.
Одиночное наследование
Механизм одиночного наследования очень прост: подкласс получает свойства только от одного основного класса:
class Pet < public void walk ()class Cow extends Pet < public void moo () >
I will walk I will moo
Многоуровневое наследование
Многоуровневый процесс происходит при наследовании дополнительного класса от базового, затем этот же класс действует уже как основной для следующего:
class Pet < public void walk ()> class Cat extends Pet < public void sleep () > class Kitty extends Cat < public void 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 ему не доступен.
Правила наследования
Написание кода подчиняется своим законам. В Java также есть определенные правила.
Обращение к методу родителя с помощью super
Чтобы обратиться к методу родителя, нужно использовать ключевое слово super.
Ефективний курс від laba: Фінансовий менеджер.
Оптимізуйте фінансовий розвиток компанії.
Давайте используем один из предыдущих вариантов-примеров, добавив super в преобразованный Cow:
class Cow extends Pet < // обращаемся к walk в Pet super. walk (); System.out.println("I will produce milk");>public void moo () < System.out.println ("I will moo");>> class TestIn< public static void main (String args[])< Cow small=new Cow(); small.walk (); small. moo (); >>
I will walk I will produce milk I will moo
В главном Pet есть walk (), также и в расширенном Сow, но уже с приставкой super.walk (), мы обращаемся к walk () в суперкласс из подкласса.
Получение доступа к защищенным элементам и методам
Если protected (защищен) стоит перед элементами и методами — это указание на то, что к ним можно обратиться из производного класса:
class Pet < protected String name; protected void display () System.out.println (“I am a pretty pet”); >class Kitty extends Pet < public void getInfo ()< System.out.println (" Call me .." + name);>> class TestIn< public static void main (String args[])< Kitty small=new Kitty(); // обращаемся к защищенному display small.name = “Lilu”; small.display (); small.getInfo(); >>
I am a pretty pet Call me Lilu
Как видно из примера, у нас есть основной класс — Pet — с защищенными name и display. Чтобы обойти эту защиту, мы создали новый объект small подкласса Kitty и получили необходимый доступ.
Наследовать можно всего один класс
Интересно, что в Java нет многократного наследования — оно возможно только от одного суперкласса.
public class extends Pet, Сow, Cat <>
Важно! В Java класс не может унаследовать ничего сам от себя.
Наследуется все, кроме приватных переменных и методов
В Java модификатор private сообщает о том, что классы-наследники не получат от родителя элементы с таким обозначением. Если вы попробуете произвести extends, компилятор выдаст ошибку.
Как переделать метод класса-родителя
Override (переопределение) — мощный инструмент, который применяется, когда необходимо изменить или переопределить реализацию метода, полученного от суперкласса.
class Cow extends Pet
Важные моменты при использовании Override:
- переопределить можно только унаследованные методы;
- название и параметры метода в родителе и наследнике — одинаковые;
- методы с обозначением private не переопределяются.
Как запретить наследование
Запретить создание подкласса можно просто применив final. Если класс отмечен как final, то ни один другой класс не может унаследовать от него элементы .
final class Pet < >class Kitty extends Pet
В результате мы получим ошибку.
Pet обозначен как final, поэтому Kitty не может получить от него ни поля ни переменных .