Наследование классов в ООП: пример и разбор

Наследование - один из трёх китов объектно-ориентированного программирования рядом с инкапсуляцией и полиморфизмом. Идея простая: новый класс берёт все поля и методы уже готового и добавляет своё, не переписывая код с нуля. Звучит абстрактно, пока не увидишь конкретный пример: класс Animal умеет «есть» и «спать», а класс Dog наследует это и добавляет метод «лаять». Разберём, как наследование записывается в разных языках, чем extends отличается от композиции, как работает вызов родительского конструктора через super и где студенты чаще всего спотыкаются.
Чтобы не теоретизировать в пустоту, соберите ниже короткий запрос под нужный язык и тип наследования - получите рабочий пример с пояснением каждой строки.
Что такое наследование классов простыми словами
Наследование - это механизм, при котором один класс (наследник, или дочерний класс) получает поля и методы другого класса (базового, или родительского) и может их использовать как свои. Базовый класс описывает общее, наследник - частное.
Бытовая аналогия: есть общее понятие «транспорт» с признаками «скорость» и «вместимость». «Автомобиль», «велосипед» и «самолёт» - это его частные виды. Каждый из них уже обладает скоростью и вместимостью просто потому, что является транспортом, и не нужно описывать эти признаки заново для каждого. Достаточно сказать «автомобиль - это транспорт» и добавить специфику: число колёс, тип двигателя.
Главный практический выигрыш - переиспользование кода и единая точка правки. Если у всех животных одинаковый метод «дышать», его пишут один раз в базовом классе Animal, а десяток наследников получают его бесплатно. Исправили логику в базовом - изменение разошлось по всем потомкам. Это прямое продолжение принципов ООП: наследование работает в связке с инкапсуляцией и полиморфизмом, образуя гибкую и расширяемую модель.

Канонический пример: Animal и Dog
Самый узнаваемый учебный пример - иерархия животных. Есть базовый класс Animal с общими полями (name, age) и методами (eat(), sleep()), и наследник Dog, который добавляет свой метод bark():
- базовый класс
Animalхранит имя и возраст, умеет «есть» и «спать»; - класс
Dogобъявляется как наследникAnimal(ключевое словоextendsв Java или двоеточие в C++); Dogавтоматически получаетname,age,eat(),sleep();Dogдобавляет собственный методbark(), которого нет у других животных.
Ключевой момент: создав объект Dog, можно вызвать у него и унаследованный eat(), и собственный bark(). Наследник - это расширенная версия родителя. При этом любой Dog остаётся Animal: связь «является» (is-a) - главный признак корректного наследования. Собака является животным, поэтому наследование здесь уместно.
Проверяйте отношение фразой «является»: «собака является животным» - да, наследуем. «У машины есть двигатель» - это «имеет» (has-a), здесь нужна не наследование, а композиция (поле-объект внутри класса).
Синтаксис в разных языках
Идея наследования одна, но запись отличается. На экзамене часто просят показать синтаксис конкретного языка.
В Java наследник объявляется через class Dog extends Animal. Множественного наследования классов нет, но есть наследование интерфейсов через implements. Вызов родительского конструктора - super(...).
В C++ запись через двоеточие и спецификатор доступа: class Dog : public Animal. C++ допускает множественное наследование (class C : public A, public B), что мощно, но порождает «ромбовидную проблему» и требует виртуальных базовых классов.
В Python базовые классы перечисляются в скобках: class Dog(Animal). Множественное наследование разрешено, порядок поиска методов определяет алгоритм MRO (C3-линеаризация). Родитель вызывается через super().__init__(...).
В C# синтаксис как в C++ через двоеточие: class Dog : Animal. Множественного наследования классов нет, как и в Java, зато есть интерфейсы.

Переопределение методов и вызов super
Наследник не обязан принимать поведение родителя как есть - он может переопределить метод, дав ему собственную реализацию. Например, базовый Animal.makeSound() печатает «звук животного», а Dog.makeSound() переопределяет его и печатает «Гав». При вызове у объекта Dog сработает именно дочерняя версия - это основа полиморфизма.
Часто переопределённый метод хочет не заменить родительский полностью, а дополнить его. Тогда внутри дочернего метода вызывают родительскую реализацию через super (в C++ - через имя базового класса, Animal::makeSound()). Самый частый случай - конструктор: дочерний конструктор сначала вызывает super(...), чтобы родитель проинициализировал свои поля, а потом инициализирует свои.
В Java и C# вызов super() в конструкторе должен идти ПЕРВОЙ строкой. Если базовый класс не имеет конструктора без аргументов, а вы забыли явно вызвать super с параметрами - компилятор выдаст ошибку. Это типичная ловушка на лабораторных.
Полиморфизм тесно связан с наследованием: переопределённые методы позволяют работать с разными наследниками через ссылку на базовый тип. Подробнее это разобрано в статье про полиморфизм в ООП на примере - наследование там выступает фундаментом.
Виды наследования
В вузовском курсе различают несколько форм, и их полезно знать для теоретического вопроса.
Одиночное (single) - наследник имеет ровно один базовый класс. Самый частый и безопасный случай: Dog extends Animal.
Многоуровневое (multilevel) - цепочка: Cat extends Animal, Kitten extends Cat. Котёнок получает всё от кошки и от животного. Цепочки длиннее трёх уровней считаются признаком переусложнённой модели.
Иерархическое (hierarchical) - у одного родителя несколько наследников: от Animal наследуют Dog, Cat, Bird. Это самый типичный рисунок реальной иерархии.
Множественное (multiple) - у наследника несколько родителей сразу. Есть в C++ и Python, запрещено для классов в Java и C# из-за «ромбовидной проблемы» (когда два родителя наследуют от общего предка и возникает неоднозначность). Java решает это через интерфейсы.
Наследование или композиция
Распространённая ошибка новичка - наследовать ради переиспользования кода, когда отношения «является» на самом деле нет. Если класс Engine (двигатель) нужен внутри Car, неверно писать Car extends Engine - машина не является двигателем, она его содержит. Здесь правильна композиция: поле типа Engine внутри Car.
Современный принцип проектирования звучит как «предпочитай композицию наследованию» (composition over inheritance): композиция гибче, не создаёт жёсткой связи между классами и не страдает от хрупкости базового класса, когда правка в родителе ломает потомков. Наследование оставляют для настоящих отношений «является».
Практический критерий выбора: если в будущем поведение объекта может меняться в рантайме или объект логически собирается из независимых частей - берите композицию. Машина с разными двигателями, документ с разными форматами вывода, персонаж с набором умений - всё это собирается из частей, а не выстраивается в жёсткую иерархию классов. Наследование же уместно, когда подтип навсегда остаётся частным случаем родителя и расширяет, а не подменяет его суть: квадрат как частный прямоугольник, сотрудник как частное лицо, целое число как частный случай числа. Когда сомневаетесь - начните с композиции: переход от композиции к наследованию обходится дешевле, чем обратный рефакторинг разросшейся иерархии.
Частые ошибки
- Наследование вместо композиции.
Stack extends ArrayList- классический антипример: стек использует список, но не является им. Лучше держать список полем. - Забытый вызов super в конструкторе. Поля родителя остаются неинициализированными или компилятор сразу ругается. В Java и C# super() обязан быть первой строкой.
- Глубокие иерархии. Цепочки в 4–5 уровней делают код хрупким: правка в корне непредсказуемо влияет на всех потомков. Держите иерархию плоской.
- Переопределение с другой сигнатурой. Если метод в наследнике отличается типами параметров, это не переопределение, а перегрузка - и полиморфизм не сработает. В Java помогает аннотация
@Override. - Наследование от конкретного класса вместо абстрактного. Если базовый класс не предназначен для наследования, лучше делать его абстрактным или интерфейсом, а не наследовать готовую реализацию.
FAQ
Чем наследование отличается от полиморфизма? Наследование - это механизм передачи полей и методов от родителя к потомку. Полиморфизм - это способность вызывать переопределённый метод через ссылку на базовый тип, чтобы один вызов работал по-разному для разных наследников. Наследование создаёт иерархию, полиморфизм её использует.
Можно ли наследоваться от нескольких классов? Зависит от языка. C++ и Python разрешают множественное наследование классов. Java и C# - нет (только один базовый класс), но позволяют реализовать несколько интерфейсов. Запрет связан с «ромбовидной проблемой» неоднозначности при общем предке.
Что такое абстрактный базовый класс? Это класс, который нельзя инстанцировать напрямую - он задаёт общий контракт (часто с абстрактными методами без тела), а конкретную реализацию дают наследники. Используется, когда базовое понятие («Фигура», «Животное») само по себе слишком общее, чтобы существовать как объект.
Коротко
Наследование позволяет новому классу получить поля и методы готового и добавить своё, переиспользуя код через отношение «является» (is-a). Базовый класс описывает общее, наследник - частное; методы можно переопределять и дополнять через super. Синтаксис отличается по языкам (extends, двоеточие, скобки), а виды наследования - от одиночного до множественного. Если связь не «является», а «содержит», вместо наследования берите композицию.
Читайте также

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

Абстрактный класс и интерфейс: в чём отличие
Абстрактный класс и интерфейс: чем отличаются в ООП, когда наследовать поведение, а когда задавать контракт, как выбрать на примерах Java, C# и Python.

Порождающие, структурные и поведенческие паттерны GoF
Чем отличаются порождающие, структурные и поведенческие паттерны проектирования, как разнести 23 шаблона GoF по группам и когда какой применять с примерами.