Наследование

  • Рубрика записи:ООП

Наследование – пожалуй, второй по простоте понимания принцип ООП (после абстракции).

Наследование – это когда классы-наследники перенимают поля и методы классов-родителей. Они могут их использовать так, как будто бы этот код был записан в них самих.

Что такое наследование и зачем

Наследование – это один из 4 китов ООП (помимо абстракции, инкапсуляции и полиморфизма).

Сразу разъясним понятия и их синонимы:

  1. Класс, который наследуется от другого класса – класс-потомок, класс-наследник, подкласс, производный класс.
  2. Класс, от которого наследуется другой класс – класс-родитель, родительский класс, суперкласс, базовый класс.

Механизм наследования позволяет одним классам ложиться в основу других классов. При этом класс-наследник имеет всё, что имеет класс-родитель, но может и добавлять новое. Ведь наследование (от английского – inheritance) также называют расширением или обобщением.

Соответственно:

  • класс-наследник расширяет класс-родитель, как понятие “Свитер” расширяет и добавляет новое, уточняет понятие “Одежда”;
  • класс-родитель обобщает класс-наследник, как понятие “Устройство” обобщает понятия “Телефон” и “Ноутбук”.

Но для чего весь сыр-бор? Вообще-то, по веским причинам.

И первая из них – это дублирование кода. Да, вам не нужно копировать весь код из класса-родителя в класс-наследник просто потому, что ему он тоже нужен. Вы просто указываете, что класс-наследник наследуется от класса-родителя и дело в шляпе.

Вторая причина – это иерархия. Классы могут быть организованы в иерархическую структуру, где более конкретные классы (“Телефон”, “Ноутбук”) наследуются от более общих (“Устройство”). Например, класс “Фрукт” может быть базовым классом, а “Яблоко” и “Апельсин” будут производными классами.

А третья причина – это способствование использованию полиморфизма. Да, наследование позволяет реализовывать полиморфизм в программах с парадигмой ООП. По секрету скажу, что это не единственный способ реализации полиморфизма, но, пожалуй, наиболее частый. Получается, объекты класса-наследника могут быть использованы там, где ожидается объект класса-родителя. Я лично была в шоке, когда узнала, что можно легально делать так:

И дальше вы можете работать с этим объектом как будто это объект Device!

Наследование в Java

В Java используется ключевое слово extends для того, чтобы показать, что один класс наследуется от другого:

Причём в Java не поддерживается множественное наследование от классов. Вы можете унаследоваться только от одного класса. Зато вы можете наследовать множество интерфейсов!

При этом, все классы в Java автоматически наследуются от класса Object. У него есть всякие методы вроде getClass(), toString(), equals(), hashCode().

Скриншот подсказок IntelliJ IDEA с методами из класса Object, от которого наследуются все классы в Java

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

Кстати говоря, например, метод toString() возвращает строковое представление объекта. Давайте на его примере рассмотрим, как это – переопределять методы.

Метод toString() уже есть в родительском классе Object. Но мы хотим изменить поведение объекта Example при вызове toString() на объекте Example. Поэтому можно просто написать свою реализацию этого метода внутри класса Example.

Желательно помечать переопределённые методы аннотацией @Override. Да, программа скомпилируется и без этого. Но так удобнее для разработчиков. Во-первых, видно, что это переопределённый метод, а не тот, что был определён изначально в классе Example. Во-вторых, аннотация @Override позволяет проверить, есть ли в родительском классе метод с таким же названием и набором параметров, пригодный для переопределения. И если нет – вы это увидите. Удобно? Удобно.

Итак, проверим, как будет работать метод toString() на объекте Example в двух ситуациях:

Без переопределения метода toString()

Вывод в консоль не переопределённого метода toString()

С переопределением на возврат строки “Объект Example”

Вывод в консоль переопределённого метода toString() с помощью аннотации @Override

Получается, в первом случае объект Example, вызывая метод toString(), вызвал первоначальную реализацию этого метода в классе Object. А во втором случае – свою собственную, так как было добавлено переопределение метода с аннотацией @Override.

Важно понимать, что private-переменные и private-методы класса-родителя не будут доступны в классах-наследниках. Нужно либо инкапсулировать логику работы с ними в protected- или public-методах, либо их самих сделать protected. Зависит от задачи.

Ещё один важный момент – это обращение к конструкторам и методам класса-родителя. Да, класс Example переопределяет метод toString() от Object, но это не мешает ему обратиться к нему при помощи ключевого слова super:

В итоге переопределённый метод toString() в классе Example попутно обратился и к родительскому методу toString(). Вот такие отцы и дети!

Вывод в консоль переопределённого метода toString() с помощью аннотации @Override и с использованием базового метода toString()

Кроме использования родительских методов через super, можно также обратиться к родительскому конструктору. Например, класс Example наследуется от класса Test (имена выбираем максимально произвольные). А у класса Test есть конструктор с одним int-ом, который будет сохранён в приватную переменную testValue.

Тогда Example может выглядеть так:

Или вот так:

Что ж, технические аспекты разобрали. Теперь можно идти за осмысленными примерами!

Примеры кода

Будет у нас программа для IT-предприятия. В ней нужно учитывать сотрудников и рассчитывать им зарплаты. Итак, базовый класс – сотрудник – Employee:

Кстати, переменная experienceInYears названа именно так, а не просто experience. Потому что содержательное имя переменной, которая может измеряться в разных величинах, должно отражать единицу измерения. Так не запутаешься.

Далее следуют два класса: менеджер (Manager) и разработчик (Developer). Они будут переопределять метод расчёта зарплаты. При этом, они всё равно будут использовать этот метод из базового класса.

Отлично. Давайте протестируем эти классы. Создадим класс Main и выведем результаты в консоль.

Вывод в консоль зарплат разных сотрудников, созданных при помощи наследования от класса Employee

Неплохие такие зарплаты у ребят. При этом классы-наследники ничего знать не знают про метод расчёта базовой части зарплаты, и как на неё влияет стаж. Разделение ответственностей, типа. Хотя этот вопрос, конечно, всегда спорный: где заканчивается одна ответственность и начинается другая? Но мы своего добились – классы написали и их работу проверили. Бинго!

Между делом, наследование в Java – это не только extends (с классами), но и implements (с интерфейсами). Гораздо чаще, чем от обычных классов, наследование используется от абстрактных классов или интерфейсов. В таком случае мы говорим об их реализации. И тогда можно построить целую иерархию классов. Но это – уже другая история.

Как минимум, наследование помогает избежать дублирования кода. Дублирование кода – это зло. Следовательно, наследование – добро?

Добавить комментарий