Включите исполнение JavaScript в браузере, чтобы запустить приложение.
15 янв 2025

Наследование в программировании

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

Что такое наследование в ООП

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

Этот механизм позволяет избежать лишнего дублирования кода, тем самым упрощая разработку — сделать код структурированным и упростить его поддержку.

Как наследовать класс

Вначале нужно создать родительский класс — тот класс, от которого будет наследоваться дочерний:

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);

  }

}
java

Это класс Person, который описывает человека, имеет свойства имя и возраст, а также метод, который выводит эту информацию на экран.

Чтобы наследовать класс, как правило, используется ключевое слово extends или двоеточие (это зависит от языка программирования):

class Student extends Person {

  }
java

Теперь класс 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;  

  }

}
java

Теперь класс Student содержит не только поля класса Person, но и собственное поле university. Также добавлен новый метод getUniversity(), который возвращает название университета студента, при этом display() не изменен.

Наследование конструкторов

В классическом понимании прямого наследования конструкторов не существует в большинстве языков программирования, но дочерние классы могут вызывать конструкторы своих «родителей». В C++ для этого используется base(), а в Java это делается с помощью super():

  Student(String name, int age, String university) {

      super(name, age);

      this.university = university;

  }
java

Это часть кода из примера выше — здесь 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);  

  }

}
java

Теперь display() класса Student будет выводить на экран данные об университете, а не имя и фамилию. Перед переопределенным методом указана аннотация @Override, хоть она и необязательна.

Наследование от класса Object

В большинстве языков программирования существует класс Object, он является родительским классом для всех элементов иерархии: все классы наследуются от него по умолчанию. За счет этой особенности все классы имеют несколько общих атрибутов и методов. Например, метод __str__ в Python или toString() в Java. При этом такие методы тоже можно переопределять при необходимости.

Виды наследования в программировании

Простое наследование

Простое наследование — это наиболее распространенный тип этого механизма в ООП. Он предполагает, что класс может наследоваться только от одного родителя (то есть подразумевается создание класса-наследника, который обладает всеми свойствами и функционалом родителя). Это упрощает и структурирует код.

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

class Bird extends Animal {

    // Какой-то код

}
java

Множественное наследование

Множественное наследование уже подразумевает, что класс-наследник может иметь более одного родителя. Этот тип имеет преимущество в виде придания коду гибкости, но также часто критикуется. Во-первых, существует мнение о том, что необходимость в реализации такого типа механизма — это следствие неверного проектирования. Во-вторых, существует проблема ромба (иногда она называется проблемой алмаза), которая связана с появлением конфликтов между методами. Поэтому данный тип поддерживается не всеми языками, например, в Java он запрещен, а в C++ и Python поддерживается.

Пример на C++:

class Camera {

    // Какой-то код

};

class Phone {

    // Какой-то код

};

class Smartphone : public Camera, public Phone {

    // Какой-то код

};
c++

Наследование по интерфейсам

Этот вид подразумевает реализацию нескольких интерфейсов одним классом. Таким образом можно частично обойти ограничение на множественное наследование, которое есть во многих языках. Интерфейсы могут определять константы и методы (они не обязательно должны иметь реализации).

Так может выглядеть определение интерфейсов Flyable и Swimmable, а также их методов fly() и swim(), которые не имеют реализации:

interface Flyable {

  void fly();

}

interface Swimmable {

  void swim();

}
bash

Модификаторы доступа fly() и swim() не указаны явно, но они имеют доступ public, так как интерфейсы предназначены для реализации их классом.

Чтобы класс реализовывал интерфейс, используется ключевое слово implements:

class Duck implements Flyable, Swimmable {

  public void fly() {

      System.out.println("Утка летит");

  }

  public void swim() {

      System.out.println("Утка плывет");

  }

}
java

Теперь 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

*/
java