Основы OOP наследования: Inheritance и Polymorphie
Наследование (Inheritance) — это центральное понятие объектно-ориентированного программирования, которое позволяет определить общие свойства и поведение в базовом классе и переиспользовать их в производных классах.
Что такое наследование?
Наследование описывает отношение “является” (is-a) между типами: производный класс наследует все публичные и защищённые члены базового класса и может их расширять или переопределять.
Ключевые концепции наследования
- Отношение “является”: производный класс — это специализация базового класса
- Переиспользование кода: общее поведение определяется один раз в центральном месте
- Полиморфизм: объекты можно использовать как объекты базового типа
- Расширяемость: новую функциональность легко добавлять в существующие иерархии
Наследование в Java
Простая иерархия наследования
// Абстрактный базовый класс
public abstract class Shape {
protected String color;
protected double x, y; // Position
public Shape(String color, double x, double y) {
this.color = Objects.requireNonNull(color);
this.x = x;
this.y = y;
}
// Абстрактные методы — должны быть реализованы
public abstract double area();
public abstract double perimeter();
// Конкретный метод — может быть переопределён
public void move(double dx, double dy) {
this.x += dx;
this.y += dy;
System.out.println("Form verschoben nach (" + x + ", " + y + ")");
}
// Getters
public String getColor() { return color; }
public double getX() { return x; }
public double getY() { return y; }
@Override
public String toString() {
return "Shape{color='" + color + "', x=" + x + ", y=" + y + "}";
}
}
// Конкретный производный класс
public class Rectangle extends Shape {
private double width, height;
public Rectangle(String color, double x, double y, double width, double height) {
super(color, x, y); // Вызов конструктора базового класса
setDimensions(width, height);
}
@Override
public double area() {
return width * height;
}
@Override
public double perimeter() {
return 2 * (width + height);
}
@Override
public void move(double dx, double dy) {
super.move(dx, dy); // Вызов метода базового класса
System.out.println("Rechteck bewegt");
}
// Дополнительные методы
public void setDimensions(double width, double height) {
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException("Maße müssen positiv sein");
}
this.width = width;
this.height = height;
}
public double getWidth() { return width; }
public double getHeight() { return height; }
@Override
public String toString() {
return "Rectangle{" + super.toString() +
", width=" + width + ", height=" + height + "}";
}
}
// Ещё один производный класс
public class Circle extends Shape {
private double radius;
public Circle(String color, double x, double y, double radius) {
super(color, x, y);
setRadius(radius);
}
@Override
public double area() {
return Math.PI * radius * radius;
}
@Override
public double perimeter() {
return 2 * Math.PI * radius;
}
public void setRadius(double radius) {
if (radius <= 0) {
throw new IllegalArgumentException("Radius muss positiv sein");
}
this.radius = radius;
}
public double getRadius() { return radius; }
@Override
public String toString() {
return "Circle{" + super.toString() + ", radius=" + radius + "}";
}
}
Полиморфное использование
public class ShapeDemo {
public static void main(String[] args) {
// Полиморфные ссылки
List<Shape> shapes = new ArrayList<>();
shapes.add(new Rectangle("rot", 0, 0, 5, 3));
shapes.add(new Circle("blau", 10, 10, 2));
shapes.add(new Rectangle("grün", 5, 5, 2, 2));
// Полиморфная обработка
double totalArea = 0;
for (Shape shape : shapes) {
System.out.println(shape);
System.out.println("Fläche: " + shape.area());
System.out.println("Umfang: " + shape.perimeter());
totalArea += shape.area();
System.out.println("---");
}
System.out.println("Gesamtfläche: " + totalArea);
// Динамические вызовы методов
processShapes(shapes);
}
public static void processShapes(List<Shape> shapes) {
for (Shape shape : shapes) {
// Динамическая диспетчеризация — в зависимости от типа объекта
shape.move(1, 1);
// Проверка типа и приведение
if (shape instanceof Rectangle) {
Rectangle rect = (Rectangle) shape;
System.out.println("Rechteck: " + rect.getWidth() + "x" + rect.getHeight());
} else if (shape instanceof Circle) {
Circle circle = (Circle) shape;
System.out.println("Kreis mit Radius: " + circle.getRadius());
}
}
}
}
Наследование в C#
Наследование и интерфейсы в C#
// Интерфейс для определения контракта
public interface IShape
{
string Color { get; }
double Area { get; }
double Perimeter { get; }
void Move(double dx, double dy);
}
// Абстрактный базовый класс
public abstract class Shape : IShape
{
protected string Color { get; set; }
protected double X { get; set; }
protected double Y { get; set; }
protected Shape(string color, double x, double y)
{
Color = color ?? throw new ArgumentNullException(nameof(color));
X = x;
Y = y;
}
// Реализация интерфейса
public string GetColor() => Color;
// Абстрактные свойства
public abstract double Area { get; }
public abstract double Perimeter { get; }
// Виртуальный метод — может быть переопределён
public virtual void Move(double dx, double dy)
{
X += dx;
Y += dy;
Console.WriteLine($"Form verschoben nach ({X}, {Y})");
}
public override string ToString()
{
return $"Shape{{Color='{Color}', X={X}, Y={Y}}}";
}
}
// Конкретный класс
public class Rectangle : Shape
{
public double Width { get; private set; }
public double Height { get; private set; }
public Rectangle(string color, double x, double y, double width, double height)
: base(color, x, y)
{
SetDimensions(width, height);
}
public override double Area => Width * Height;
public override double Perimeter => 2 * (Width + Height);
public override void Move(double dx, double dy)
{
base.Move(dx, dy);
Console.WriteLine("Rechteck bewegt");
}
public void SetDimensions(double width, double height)
{
if (width <= 0 || height <= 0)
throw new ArgumentException("Maße müssen positiv sein");
Width = width;
Height = height;
}
public override string ToString()
{
return $"Rectangle{{{base.ToString()}, Width={Width}, Height={Height}}}";
}
}
Наследование в Python
from abc import ABC, abstractmethod
from typing import List
# Абстрактный базовый класс
class Shape(ABC):
def __init__(self, color: str, x: float, y: float):
if not color:
raise ValueError("Color дarf nicht leer sein")
self.color = color
self.x = x
self.y = y
@abstractmethod
def area(self) -> float:
"""Berechnet die Fläche der Form"""
pass
@abstractmethod
def perimeter(self) -> float:
"""Berechnet den Umfang der Form"""
pass
def move(self, dx: float, dy: float):
"""Verschiebt die Form"""
self.x += dx
self.y += dy
print(f"Form verschoben nach ({self.x}, {self.y})")
def __str__(self):
return f"Shape{{color='{self.color}', x={self.x}, y={self.y}}}"
# Конкретный подкласс
class Rectangle(Shape):
def __init__(self, color: str, x: float, y: float, width: float, height: float):
super().__init__(color, x, y) # Aufruf des Basisklassen-Konstruktors
self.set_dimensions(width, height)
def area(self) -> float:
return self.width * self.height
def perimeter(self) -> float:
return 2 * (self.width + self.height)
def move(self, dx: float, dy: float):
super().move(dx, dy) # Basismethode aufrufen
print("Rechteck bewegt")
def set_dimensions(self, width: float, height: float):
if width <= 0 or height <= 0:
raise ValueError("Maße müssen positiv sein")
self.width = width
self.height = height
def __str__(self):
return f"Rectangle{{{super().__str__()}, width={self.width}, height={self.height}}}"
# Еще один подкласс
class Circle(Shape):
def __init__(self, color: str, x: float, y: float, radius: float):
super().__init__(color, x, y)
self.set_radius(radius)
def area(self) -> float:
return math.pi * self.radius ** 2
def perimeter(self) -> float:
return 2 * math.pi * self.radius
def set_radius(self, radius: float):
if radius <= 0:
raise ValueError("Radius muss positiv sein")
self.radius = radius
def __str__(self):
return f"Circle{{{super().__str__()}, radius={self.radius}}}"
# Полиморфное использование
def process_shapes(shapes: List[Shape]):
total_area = 0
for shape in shapes:
print(shape)
print(f"Fläche: {shape.area():.2f}")
print(f"Umfang: {shape.perimeter():.2f}")
total_area += shape.area()
# Type Checking mit isinstance
if isinstance(shape, Rectangle):
print(f"Rechteck: {shape.width}x{shape.height}")
elif isinstance(shape, Circle):
print(f"Kreis mit Radius: {shape.radius}")
print("---")
print(f"Gesamtfläche: {total_area:.2f}")
# Verwendung
shapes = [
Rectangle("rot", 0, 0, 5, 3),
Circle("blau", 10, 10, 2),
Rectangle("grün", 5, 5, 2, 2)
]
process_shapes(shapes)
Liskov Substitution Principle (LSP)
Понимание принципа
Принцип подстановки Лисков гласит, что подклассы должны корректно заменять свои базовые классы без неожиданного изменения поведения программы.
Пример нарушения LSP
// Плохой дизайн - нарушает LSP
public class Rectangle {
protected double width, height;
public void setWidth(double width) {
this.width = width;
}
public void setHeight(double height) {
this.height = height;
}
public double getWidth() { return width; }
public double getHeight() { return height; }
public double area() {
return width * height;
}
}
// Проблемный подкласс
public class Square extends Rectangle {
@Override
public void setWidth(double width) {
super.setWidth(width);
super.setHeight(width); // Квадрат должен иметь равные стороны
}
@Override
public void setHeight(double height) {
super.setWidth(height);
super.setHeight(height);
}
}
// Нарушение LSP на практике
public void testRectangle(Rectangle rect) {
rect.setWidth(5);
rect.setHeight(4);
// Ожидание: area() == 20
// При Square: area() == 16 (неожиданно!)
assert rect.area() == 20 : "LSP verletzt!";
}
Конформный LSP дизайн
// Лучше: абстрактный базовый класс
public abstract class Shape {
public abstract double area();
public abstract double perimeter();
}
// Rectangle как самостоятельный класс
public class Rectangle extends Shape {
private double width, height;
public Rectangle(double width, double height) {
setDimensions(width, height);
}
public void setDimensions(double width, double height) {
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException("Maße müssen positiv sein");
}
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
@Override
public double perimeter() {
return 2 * (width + height);
}
public double getWidth() { return width; }
public double getHeight() { return height; }
}
// Square как самостоятельный класс
public class Square extends Shape {
private double side;
public Square(double side) {
setSide(side);
}
public void setSide(double side) {
if (side <= 0) {
throw new IllegalArgumentException("Seite muss positiv sein");
}
this.side = side;
}
@Override
public double area() {
return side * side;
}
@Override
public double perimeter() {
return 4 * side;
}
public double getSide() { return side; }
}
Композиция вместо наследования
Проблема жестких иерархий
// Плохо: глубокая иерархия наследования
public class Animal {
public void eat() { System.out.println("Eating"); }
}
public class Mammal extends Animal {
public void walk() { System.out.println("Walking"); }
}
public class Dog extends Mammal {
public void bark() { System.out.println("Barking"); }
}
public class RobotDog extends Dog {
// Проблема: RobotDog не является настоящим животным!
@Override
public void eat() {
throw new UnsupportedOperationException("Robots don't eat");
}
}
Лучше: композиция
// Интерфейсы для поведения
public interface Eater {
void eat();
}
public interface Walker {
void walk();
}
public interface Barker {
void bark();
}
// Базовый класс с основным поведением
public class Animal implements Eater {
protected String name;
public Animal(String name) {
this.name = name;
}
@Override
public void eat() {
System.out.println(name + " is eating");
}
}
// Композиция для поведений
public class Dog implements Walker, Barker {
private Animal animal;
public Dog(String name) {
this.animal = new Animal(name);
}
@Override
public void walk() {
System.out.println(animal.name + " is walking");
}
@Override
public void bark() {
System.out.println(animal.name + " says: Woof!");
}
// Делегирование к Animal
public void eat() {
animal.eat();
}
}
// Более гибко для RobotDog
public class RobotDog implements Walker, Barker {
private String name;
private int batteryLevel;
public RobotDog(String name) {
this.name = name;
this.batteryLevel = 100;
}
@Override
public void walk() {
if (batteryLevel > 10) {
System.out.println(name + " is walking on wheels");
batteryLevel -= 5;
} else {
System.out.println(name + " needs charging");
}
}
@Override
public void bark() {
System.out.println(name + " says: Electronic Woof!");
}
public void charge() {
batteryLevel = 100;
System.out.println(name + " is fully charged");
}
}
Абстрактные классы против интерфейсов
Абстрактный класс
// Абстрактный класс с общей реализацией
public abstract class Vehicle {
protected String brand;
protected int year;
public Vehicle(String brand, int year) {
this.brand = brand;
this.year = year;
}
// Конкретный метод
public void startEngine() {
System.out.println("Engine starting...");
}
// Абстрактные методы
public abstract void accelerate();
public abstract void brake();
// Template Method Pattern
public final void drive() {
startEngine();
accelerate();
System.out.println("Driving...");
brake();
}
// Геттеры
public String getBrand() { return brand; }
public int getYear() { return year; }
}
public class Car extends Vehicle {
public Car(String brand, int year) {
super(brand, year);
}
@Override
public void accelerate() {
System.out.println("Car accelerating");
}
@Override
public void brake() {
System.out.println("Car braking");
}
}
Интерфейс
// Интерфейс для поведения
public interface Electric {
void charge();
int getBatteryLevel();
}
public interface Autonomous {
void enableAutopilot();
boolean isAutopilotActive();
}
// Класс реализует несколько интерфейсов
public class Tesla extends Vehicle implements Electric, Autonomous {
private int batteryLevel = 80;
private boolean autopilotActive = false;
public Tesla(String brand, int year) {
super(brand, year);
}
@Override
public void accelerate() {
System.out.println("Tesla accelerating silently");
}
@Override
public void brake() {
System.out.println("Tesla regenerative braking");
}
// Реализация интерфейсов
@Override
public void charge() {
System.out.println("Tesla charging...");
batteryLevel = 100;
}
@Override
public int getBatteryLevel() {
return batteryLevel;
}
@Override
public void enableAutopilot() {
autopilotActive = true;
System.out.println("Autopilot enabled");
}
@Override
public boolean isAutopilotActive() {
return autopilotActive;
}
}
Проблема ромба
Множественное наследование в C++
#include <iostream>
class Animal {
public:
void eat() { std::cout << "Animal eating" << std::endl; }
};
class Mammal : virtual public Animal {
public:
void walk() { std::cout << "Mammal walking" << std::endl; }
};
class Bird : virtual public Animal {
public:
void fly() { std::cout << "Bird flying" << std::endl; }
};
// Проблема ромба решена виртуальным наследованием
class Bat : public Mammal, public Bird {
public:
void echolocate() { std::cout << "Bat echolocating" << std::endl; }
};
int main() {
Bat bat;
bat.eat(); // Однозначно благодаря виртуальному наследованию
bat.walk(); // Из Mammal
bat.fly(); // Из Bird
bat.echolocate(); // Собственный метод
return 0;
}
Решение на основе интерфейсов в Java/C#
// Интерфейс для способностей
public interface Flyable {
void fly();
}
public interface Walkable {
void walk();
}
// Базовый класс
public abstract class Animal {
public abstract void eat();
}
// Класс реализует несколько интерфейсов
public class Bat extends Animal implements Flyable, Walkable {
@Override
public void eat() {
System.out.println("Bat eating insects");
}
@Override
public void fly() {
System.out.println("Bat flying");
}
@Override
public void walk() {
System.out.println("Bat walking");
}
public void echolocate() {
System.out.println("Bat echolocating");
}
}
Лучшие практики для наследования
1. Избегайте глубоких иерархий
// Плохо: слишком глубоко
class Animal -> Mammal -> Dog -> Labrador -> GoldenRetriever
// Лучше: более плоская структура
class Animal -> Dog
class Dog -> Labrador
class Dog -> GoldenRetriever
2. Используйте final для стабильных классов
public final class ImmutablePoint {
private final double x, y;
public ImmutablePoint(double x, double y) {
this.x = x;
this.y = y;
}
// Не может быть унаследован - гарантирует стабильность
}
3. Template Method Pattern
public abstract class DataProcessor {
// Template Method, определяющий последовательность
public final void processData() {
loadData();
validateData();
transformData();
saveData();
cleanup();
}
// Конкретные методы
private void loadData() {
System.out.println("Loading data...");
}
private void cleanup() {
System.out.println("Cleaning up...");
}
// Абстрактные методы, реализуемые подклассами
protected abstract void validateData();
protected abstract void transformData();
protected abstract void saveData();
}
public class CSVProcessor extends DataProcessor {
@Override
protected void validateData() {
System.out.println("Validating CSV data");
}
@Override
protected void transformData() {
System.out.println("Transforming CSV data");
}
@Override
protected void saveData() {
System.out.println("Saving CSV data");
}
}
Концепции для экзамена
Важные различия
-
Переопределение против перегрузки
- Переопределение: одна и та же сигнатура в подклассе
- Перегрузка: одно имя, разные параметры
-
Абстрактный класс против интерфейса
- Абстрактный класс: общая реализация
- Интерфейс: только контракт
-
Отношение “является” против “имеет”
- Является: наследование
- Имеет: композиция
-
Ковариантность против контравариантности
- Ковариантность: типы возврата могут быть более специфичными
- Контравариантность: типы параметров могут быть более общими
Типичные экзаменационные задачи
// Пример полиморфизма
public class PolymorphismDemo {
public static void main(String[] args) {
Shape[] shapes = {
new Rectangle("красный", 0, 0, 5, 3),
new Circle("синий", 10, 10, 2)
};
for (Shape shape : shapes) {
// Динамическая диспетчеризация
System.out.println(shape.area());
// Type Casting
if (shape instanceof Rectangle) {
Rectangle rect = (Rectangle) shape;
System.out.println("Width: " + rect.getWidth());
}
}
}
}
Заключение
Наследование - это мощный инструмент, но требует осторожного применения:
- Используйте наследование для настоящих отношений “является”
- Соблюдайте принцип подстановки Лисков
- Предпочитайте композицию при простом переиспользовании кода
- Держите иерархии плоскими и стабильными
- Используйте интерфейсы для гибких контрактов
- Применяйте абстрактные классы для общей реализации
Хорошее наследование способствует переиспользованию и полиморфизму, а плохое наследование приводит к жесткой связанности и хрупким архитектурам.
Продолжение пути обучения OOP
Все статьи по OOP теперь полностью готовы. Вернитесь к первой статье: Основы объектно-ориентированного программирования OOP.

