Что такое наследование в ООП
Наследование — это принцип объектно-ориентированного программирования (ООП), который позволяет создавать иерархии классов: дочерние классы могут наследовать данные и методы от родительских классов. При этом дочерние классы также могут расширять функциональность родительских — иметь собственные поля и методы.
Этот механизм позволяет избежать лишнего дублирования кода, тем самым упрощая разработку — сделать код структурированным и упростить его поддержку.
Как наследовать класс
Вначале нужно создать родительский класс — тот класс, от которого будет наследоваться дочерний:
class Person {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
void display() {
System.out.println("Имя: " + name);
System.out.println("Возраст: " + age);
}
}
Это класс Person, который описывает человека, имеет свойства имя и возраст, а также метод, который выводит эту информацию на экран.
Чтобы наследовать класс, как правило, используется ключевое слово extends или двоеточие (это зависит от языка программирования):
class Student extends Person {
}
Теперь класс Student содержит поля суперкласса Person и может вызывать его методы.
Добавление новых полей и методов
Чтобы расширить функциональность родительского класса, можно добавить в дочерний класс новые поля и методы. Например, классу Student не хватает поля university:
class Student extends Person {
String university;
Student(String name, int age, String university) {
super(name, age);
this.university = university;
}
String getUniversity() {
return university;
}
}
Теперь класс Student содержит не только поля класса Person, но и собственное поле university. Также добавлен новый метод getUniversity(), который возвращает название университета студента, при этом display() не изменен.
Наследование конструкторов
В классическом понимании прямого наследования конструкторов не существует в большинстве языков программирования, но дочерние классы могут вызывать конструкторы своих «родителей». В C++ для этого используется base(), а в Java это делается с помощью super():
Student(String name, int age, String university) {
super(name, age);
this.university = university;
}
Это часть кода из примера выше — здесь super(name, age) вызывает конструктор родительского класса Person.
Переопределение методов
Дочерние классы могут переопределять методы суперклассов. Например, так можно изменить метод display() суперкласса Person:
class Student extends Person {
String university;
Student(String name, int age, String university) {
super(name, age);
this.university = university;
}
@Override
void display() {
System.out.println("Университет: " + university);
}
}
Теперь display() класса Student будет выводить на экран данные об университете, а не имя и фамилию. Перед переопределенным методом указана аннотация @Override, хоть она и необязательна.
Наследование от класса Object
В большинстве языков программирования существует класс Object, он является родительским классом для всех элементов иерархии: все классы наследуются от него по умолчанию. За счет этой особенности все классы имеют несколько общих атрибутов и методов. Например, метод __str__ в Python или toString() в Java. При этом такие методы тоже можно переопределять при необходимости.
Виды наследования в программировании
Простое наследование
Простое наследование — это наиболее распространенный тип этого механизма в ООП. Он предполагает, что класс может наследоваться только от одного родителя (то есть подразумевается создание класса-наследника, который обладает всеми свойствами и функционалом родителя). Это упрощает и структурирует код.
Это может выглядеть так:
class Bird extends Animal {
// Какой-то код
}
Множественное наследование
Множественное наследование уже подразумевает, что класс-наследник может иметь более одного родителя. Этот тип имеет преимущество в виде придания коду гибкости, но также часто критикуется. Во-первых, существует мнение о том, что необходимость в реализации такого типа механизма — это следствие неверного проектирования. Во-вторых, существует проблема ромба (иногда она называется проблемой алмаза), которая связана с появлением конфликтов между методами. Поэтому данный тип поддерживается не всеми языками, например, в Java он запрещен, а в C++ и Python поддерживается.
Пример на C++:
class Camera {
// Какой-то код
};
class Phone {
// Какой-то код
};
class Smartphone : public Camera, public Phone {
// Какой-то код
};
Наследование по интерфейсам
Этот вид подразумевает реализацию нескольких интерфейсов одним классом. Таким образом можно частично обойти ограничение на множественное наследование, которое есть во многих языках. Интерфейсы могут определять константы и методы (они не обязательно должны иметь реализации).
Так может выглядеть определение интерфейсов Flyable и Swimmable, а также их методов fly() и swim(), которые не имеют реализации:
interface Flyable {
void fly();
}
interface Swimmable {
void swim();
}
Модификаторы доступа fly() и swim() не указаны явно, но они имеют доступ public, так как интерфейсы предназначены для реализации их классом.
Чтобы класс реализовывал интерфейс, используется ключевое слово implements:
class Duck implements Flyable, Swimmable {
public void fly() {
System.out.println("Утка летит");
}
public void swim() {
System.out.println("Утка плывет");
}
}
Теперь Duck реализует два интерфейса.
Как наследование поддерживает полиморфизм
Полиморфизм — это способность объектов разных классов одной иерархии по-разному обрабатывать одни и те же вызовы методов. Наследование поддерживает полиморфизм через возможность переопределять методы классов. Например, в классе Person был метод display(), который выводит на экран имя и фамилию человека. В классе Student display() был переопределен — все экземпляры этого класса будут отвечать на его вызов специфичным образом.
Особенности наследования
- Уровень доступа дочерних классов к методам родительского определяется модификатором доступа: методы с доступом private недоступны дочерним классам. Также классы с модификатором доступа public не могут наследоваться от суперкласса с доступом private.
- Разработчику важно учитывать зависимости между классами, поэтому лучше избегать наследования от класса, который уже является дочерним.
- Множественное наследование — это механизм, который имеет свои плюсы, но также может вызвать проблемы в программе. Часто решить задачу можно без применения этого механизма.
- Следование принципам SOLID и, в частности, принципу подстановки Лисков позволит избежать неожиданного поведения программы.
Преимущества принципа наследования
- Переиспользование кода. Это очевидное преимущество обусловлено тем, что необходимый функционал можно один раз реализовать в суперклассе, а затем использовать во множестве дочерних.
- Упрощение самого кода и его поддержки. Наследование позволяет разбивать код на небольшие части с четко определенным функциями — такой код более читаемый и управляемый. Также если нужно что-то поменять, то можно внести изменения в суперкласс и они будут применены ко всем дочерним.
- Поддержка полиморфизма.
Пример наследования в ООП
Рассмотрим полноценный пример на языке программирования Java:
// Создание класса Phone со свойством бренд и display(), который выводит название бренда телефона
class Phone {
String brand;
Phone(String brand) {
this.brand = brand;
}
void display() {
System.out.println("Бренд устройства: " + brand);
}
}
/* Создание дочернего класса Smartphone, в который добавлено новое свойство, хранящее данные о емкости
памяти смартфона
Также добавлен метод, выводящий эту информацию об устройстве */
class Smartphone extends Phone {
int storageCapacity;
Smartphone(String brand, int storageCapacity) {
super(brand); // Бренд смартфона передается в конструктор суперкласса
this.storageCapacity = storageCapacity;
}
@Override
void display() {
super.display(); // Вызов display() суперкласса
System.out.println("Объем памяти смартфона: " + storageCapacity + "GB");
}
}
// Создание объекта класса Smartphone (бренд — Xiaomi, объем памяти — 16 ГБ)
public class Main {
public static void main(String[] args) {
Smartphone smartphone = new Smartphone("Xiaomi", 16);
smartphone.display(); //Вызов display()
}
}
/* Вывод:
Бренд устройства: Xiaomi
Объем памяти смартфона: 16GB
*/