Skip to content

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

1. Введение

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

2. Основные понятия

  • Суперкласс (родительский класс) - класс, от которого производится наследование
  • Подкласс (дочерний класс) - класс, который наследует характеристики суперкласса
  • Ключевое слово extends используется для реализации наследования

3. Синтаксис наследования

class Родитель {
    // поля и методы родительского класса
}

class Потомок extends Родитель {
    // дополнительные поля и методы
}

4. Практический пример

Рассмотрим простой пример наследования:

// Родительский класс
class Animal {
    protected String name;
    protected int age;

    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void eat() {
        System.out.println(name + " кушает");
    }

    public void sleep() {
        System.out.println(name + " спит");
    }
}

// Дочерний класс
class Dog extends Animal {
    private String breed;

    public Dog(String name, int age, String breed) {
        super(name, age); // вызов конструктора родительского класса
        this.breed = breed;
    }

    public void bark() {
        System.out.println(name + " лает: Гав-гав!");
    }
}

5. Важные аспекты наследования

5.1 Ключевое слово super

  • Используется для вызова конструктора родительского класса
  • Позволяет обращаться к методам родительского класса
class Dog extends Animal {
    public void makeSound() {
        super.eat(); // вызов метода родительского класса
        bark(); // вызов собственного метода
    }
}

5.2 Переопределение методов

class Cat extends Animal {
    @Override // аннотация, указывающая на переопределение метода
    public void eat() {
        System.out.println(name + " аккуратно кушает рыбку");
    }
}

5.3 Модификаторы доступа при наследовании

  • public - доступен везде
  • protected - доступен внутри пакета и в подклассах
  • private - не наследуется
  • default (пакетный) - доступен только внутри пакета

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

Java не поддерживает множественное наследование классов, но позволяет реализовывать множество интерфейсов:

interface Swimmable {
    void swim();
}

interface Flyable {
    void fly();
}

class Duck extends Animal implements Swimmable, Flyable {
    public Duck(String name, int age) {
        super(name, age);
    }

    @Override
    public void swim() {
        System.out.println(name + " плавает в пруду");
    }

    @Override
    public void fly() {
        System.out.println(name + " летит по небу");
    }
}

7. Практическое применение

Рассмотрим более сложный пример с иерархией классов:

// Базовый класс для транспортных средств
class Vehicle {
    protected String brand;
    protected String model;
    protected int year;

    public Vehicle(String brand, String model, int year) {
        this.brand = brand;
        this.model = model;
        this.year = year;
    }

    public void start() {
        System.out.println("Транспортное средство заводится");
    }

    public void stop() {
        System.out.println("Транспортное средство останавливается");
    }
}

// Класс для легковых автомобилей
class Car extends Vehicle {
    private int numberOfDoors;

    public Car(String brand, String model, int year, int numberOfDoors) {
        super(brand, model, year);
        this.numberOfDoors = numberOfDoors;
    }

    @Override
    public void start() {
        System.out.println("Автомобиль " + brand + " " + model + " заводится с помощью ключа");
    }

    public void honk() {
        System.out.println("Би-бип!");
    }
}

// Класс для электромобилей
class ElectricCar extends Car {
    private int batteryCapacity;

    public ElectricCar(String brand, String model, int year, int numberOfDoors, int batteryCapacity) {
        super(brand, model, year, numberOfDoors);
        this.batteryCapacity = batteryCapacity;
    }

    @Override
    public void start() {
        System.out.println("Электромобиль " + brand + " " + model + " включается кнопкой");
    }

    public void charge() {
        System.out.println("Электромобиль заряжается");
    }
}

8. Ограничения наследования

  1. Класс в Java может наследоваться только от одного класса
  2. Приватные члены класса не наследуются
  3. Конструкторы не наследуются, но могут вызываться с помощью super()
  4. Финальные классы (с модификатором final) не могут быть унаследованы

9. Преимущества наследования

  1. Повторное использование кода
  2. Установление отношений между классами
  3. Полиморфное поведение
  4. Улучшение поддерживаемости кода

10. Практические задания для закрепления:

  1. Создайте базовый класс Shape с методами для вычисления площади и периметра
  2. Создайте производные классы Circle, Rectangle и Triangle
  3. Реализуйте в каждом классе соответствующие методы расчета
  4. Продемонстрируйте использование полиморфизма
abstract class Shape {
    abstract double getArea();
    abstract double getPerimeter();
}

class Circle extends Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    double getArea() {
        return Math.PI * radius * radius;
    }

    @Override
    double getPerimeter() {
        return 2 * Math.PI * radius;
    }
}

class Rectangle extends Shape {
    private double width;
    private double height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
    double getArea() {
        return width * height;
    }

    @Override
    double getPerimeter() {
        return 2 * (width + height);
    }
}

11. Абстрактные классы и наследование

Абстрактные классы играют важную роль в наследовании, предоставляя базовый функционал для подклассов.

abstract class Employee {
    protected String name;
    protected double salary;

    public Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }

    // Абстрактный метод - должен быть реализован в подклассах
    abstract double calculateBonus();

    // Обычный метод
    public void displayInfo() {
        System.out.println("Имя: " + name);
        System.out.println("Зарплата: " + salary);
    }
}

class Manager extends Employee {
    private int teamSize;

    public Manager(String name, double salary, int teamSize) {
        super(name, salary);
        this.teamSize = teamSize;
    }

    @Override
    double calculateBonus() {
        return salary * 0.15 + (teamSize * 1000); // бонус зависит от размера команды
    }
}

class Developer extends Employee {
    private int linesOfCode;

    public Developer(String name, double salary, int linesOfCode) {
        super(name, salary);
        this.linesOfCode = linesOfCode;
    }

    @Override
    double calculateBonus() {
        return salary * 0.1 + (linesOfCode * 0.1); // бонус зависит от написанного кода
    }
}

12. Композиция vs Наследование

Иногда вместо наследования лучше использовать композицию. Пример:

// Использование наследования (не всегда оптимально)
class ElectricCar extends Car {
    private Battery battery;
}

// Использование композиции (часто лучше)
class ModernCar {
    private Engine engine;
    private Battery battery;
    private Electronics electronics;

    public ModernCar(Engine engine, Battery battery, Electronics electronics) {
        this.engine = engine;
        this.battery = battery;
        this.electronics = electronics;
    }
}

13. Наследование и исключения

При переопределении методов важно учитывать правила работы с исключениями:

class Parent {
    void method() throws IOException {
        // код
    }
}

class Child extends Parent {
    @Override
    void method() throws FileNotFoundException { // ОК: FileNotFoundException является подклассом IOException
        // код
    }
}

14. Правило подстановки Лисков (LSP)

Важный принцип наследования - объекты базового класса должны быть заменяемы объектами его подтипов:

class Bird {
    void fly() {
        System.out.println("Птица летит");
    }
}

class Sparrow extends Bird {
    @Override
    void fly() {
        System.out.println("Воробей летит");
    }
}

class Penguin extends Bird { // Нарушение LSP
    @Override
    void fly() {
        throw new UnsupportedOperationException("Пингвины не летают!");
    }
}

// Лучше переработать иерархию:
abstract class Bird {
    abstract void move();
}

class FlyingBird extends Bird {
    @Override
    void move() {
        System.out.println("Птица летит");
    }
}

class SwimmingBird extends Bird {
    @Override
    void move() {
        System.out.println("Птица плывет");
    }
}

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

class Outer {
    class Inner {
        void method() {
            System.out.println("Метод внутреннего класса");
        }
    }
}

class ChildOuter extends Outer {
    class ChildInner extends Inner {
        @Override
        void method() {
            System.out.println("Переопределенный метод");
        }
    }
}

16. Практический пример: Система управления банковскими счетами

abstract class Account {
    protected double balance;
    protected String accountNumber;

    public Account(String accountNumber, double initialBalance) {
        this.accountNumber = accountNumber;
        this.balance = initialBalance;
    }

    public abstract void withdraw(double amount);

    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            System.out.println("Внесено: " + amount);
        }
    }

    public double getBalance() {
        return balance;
    }
}

class SavingsAccount extends Account {
    private double interestRate;

    public SavingsAccount(String accountNumber, double initialBalance, double interestRate) {
        super(accountNumber, initialBalance);
        this.interestRate = interestRate;
    }

    @Override
    public void withdraw(double amount) {
        if (amount <= balance) {
            balance -= amount;
            System.out.println("Снято: " + amount);
        } else {
            System.out.println("Недостаточно средств");
        }
    }

    public void addInterest() {
        double interest = balance * interestRate;
        deposit(interest);
    }
}

class CheckingAccount extends Account {
    private double overdraftLimit;

    public CheckingAccount(String accountNumber, double initialBalance, double overdraftLimit) {
        super(accountNumber, initialBalance);
        this.overdraftLimit = overdraftLimit;
    }

    @Override
    public void withdraw(double amount) {
        if (amount <= balance + overdraftLimit) {
            balance -= amount;
            System.out.println("Снято: " + amount);
        } else {
            System.out.println("Превышен лимит овердрафта");
        }
    }
}

17. Тестирование кода с наследованием

public class BankTest {
    public static void main(String[] args) {
        // Создаем различные типы счетов
        SavingsAccount savings = new SavingsAccount("SA001", 1000.0, 0.05);
        CheckingAccount checking = new CheckingAccount("CA001", 500.0, 1000.0);

        // Тестируем операции
        savings.deposit(500.0);
        savings.withdraw(200.0);
        savings.addInterest();

        checking.withdraw(800.0); // Уходим в овердрафт

        // Демонстрация полиморфизма
        Account[] accounts = {savings, checking};
        for (Account account : accounts) {
            System.out.println("Баланс счета: " + account.getBalance());
        }
    }
}

18. Наследование и инициализация объектов

Важно понимать порядок инициализации при наследовании:

class Parent {
    // 1. Статические блоки родительского класса
    static {
        System.out.println("1. Статический блок Parent");
    }

    // 2. Нестатические блоки родительского класса
    {
        System.out.println("2. Блок инициализации Parent");
    }

    // 3. Конструктор родительского класса
    public Parent() {
        System.out.println("3. Конструктор Parent");
    }
}

class Child extends Parent {
    // 4. Статические блоки дочернего класса
    static {
        System.out.println("4. Статический блок Child");
    }

    // 5. Нестатические блоки дочернего класса
    {
        System.out.println("5. Блок инициализации Child");
    }

    // 6. Конструктор дочернего класса
    public Child() {
        System.out.println("6. Конструктор Child");
    }
}

19. Наследование и коллекции

Рассмотрим, как работает наследование с коллекциями:

class AnimalShelter {
    private List<Animal> animals = new ArrayList<>();

    public void addAnimal(Animal animal) {
        animals.add(animal);
    }

    // Демонстрация ковариантности
    public <T extends Animal> List<T> getAnimalsByType(Class<T> type) {
        return animals.stream()
            .filter(type::isInstance)
            .map(type::cast)
            .collect(Collectors.toList());
    }
}

// Использование:
AnimalShelter shelter = new AnimalShelter();
shelter.addAnimal(new Dog("Рекс", 3, "Овчарка"));
shelter.addAnimal(new Cat("Мурка", 2));

List<Dog> dogs = shelter.getAnimalsByType(Dog.class);

20. Защита от наследования

20.1 Финальные методы

class SecuritySystem {
    // Этот метод нельзя переопределить в подклассах
    final void authenticate() {
        // Критически важная логика аутентификации
        performSecurityCheck();
        validateCredentials();
    }

    private void performSecurityCheck() {
        // Внутренняя реализация
    }

    protected void validateCredentials() {
        // Можно переопределить этот метод
    }
}

20.2 Запечатанные классы (Sealed Classes) - Java 17+

// Определяем, какие классы могут наследоваться
public sealed class Shape permits Circle, Rectangle, Triangle {
    // общий код для фигур
}

// Должен быть final, sealed или non-sealed
public final class Circle extends Shape {
    // реализация круга
}

public final class Rectangle extends Shape {
    // реализация прямоугольника
}

public final class Triangle extends Shape {
    // реализация треугольника
}

21. Наследование и паттерны проектирования

21.1 Шаблонный метод (Template Method)

abstract class DataMiner {
    // Шаблонный метод
    public final void mine() {
        openFile();
        extractData();
        parseData();
        analyzeData();
        sendReport();
        closeFile();
    }

    abstract void openFile();
    abstract void extractData();

    // Методы с реализацией по умолчанию
    void parseData() {
        System.out.println("Парсинг данных по умолчанию");
    }

    void analyzeData() {
        System.out.println("Анализ данных по умолчанию");
    }

    // Хук-методы
    protected boolean shouldSendReport() {
        return true;
    }

    private void sendReport() {
        if (shouldSendReport()) {
            System.out.println("Отправка отчета");
        }
    }

    void closeFile() {
        System.out.println("Закрытие файла");
    }
}

class PDFDataMiner extends DataMiner {
    @Override
    void openFile() {
        System.out.println("Открытие PDF файла");
    }

    @Override
    void extractData() {
        System.out.println("Извлечение данных из PDF");
    }

    @Override
    protected boolean shouldSendReport() {
        return false; // Переопределение хук-метода
    }
}

22. Наследование и обобщенные типы (Generics)

class Container<T> {
    private T value;

    public Container(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}

// Наследование с сохранением типа
class SpecialContainer<T> extends Container<T> {
    public SpecialContainer(T value) {
        super(value);
    }

    public void printInfo() {
        System.out.println("Значение: " + getValue());
    }
}

// Наследование с ограничением типа
class NumberContainer<T extends Number> extends Container<T> {
    public NumberContainer(T value) {
        super(value);
    }

    public double getDoubleValue() {
        return getValue().doubleValue();
    }
}

23. Проблемы и антипаттерны наследования

23.1 Глубокая иерархия наследования

// Плохой пример - слишком глубокая иерархия
class Vehicle {}
class LandVehicle extends Vehicle {}
class Car extends LandVehicle {}
class SportsCar extends Car {}
class RaceCar extends SportsCar {}
class FormulaOneCar extends RaceCar {}

// Лучше использовать композицию
class Car {
    private Engine engine;
    private Transmission transmission;
    private List<Feature> features;
}

23.2 Наследование ради кода, а не из-за отношений "является"

// Плохой пример
class ArrayList {
    // методы работы со списком
}

class Stack extends ArrayList { // Неправильно! Стек не является списком
    // методы стека
}

// Правильный пример
class Stack {
    private ArrayList<Object> items; // Композиция
    // методы стека, делегирующие работу списку
}

24. Практическое задание: Создание системы учета товаров

abstract class Product {
    protected String name;
    protected double price;
    protected String sku; // Stock Keeping Unit

    public Product(String name, double price, String sku) {
        this.name = name;
        this.price = price;
        this.sku = sku;
    }

    abstract double calculateTax();
    abstract int getWarrantyPeriod(); // в месяцах

    public double getFinalPrice() {
        return price + calculateTax();
    }
}

class Electronics extends Product {
    private String brand;
    private int warrantyPeriod;

    public Electronics(String name, double price, String sku, String brand, int warrantyPeriod) {
        super(name, price, sku);
        this.brand = brand;
        this.warrantyPeriod = warrantyPeriod;
    }

    @Override
    double calculateTax() {
        return price * 0.2; // 20% налог на электронику
    }

    @Override
    int getWarrantyPeriod() {
        return warrantyPeriod;
    }
}

class Book extends Product {
    private String author;
    private String isbn;

    public Book(String name, double price, String sku, String author, String isbn) {
        super(name, price, sku);
        this.author = author;
        this.isbn = isbn;
    }

    @Override
    double calculateTax() {
        return price * 0.05; // 5% налог на книги
    }

    @Override
    int getWarrantyPeriod() {
        return 0; // книги не имеют гарантии
    }
}

// Система управления инвентарем
class InventorySystem {
    private Map<String, Product> inventory = new HashMap<>();

    public void addProduct(Product product) {
        inventory.put(product.sku, product);
    }

    public double calculateTotalValue() {
        return inventory.values().stream()
            .mapToDouble(Product::getFinalPrice)
            .sum();
    }

    public List<Product> getProductsWithWarranty() {
        return inventory.values().stream()
            .filter(p -> p.getWarrantyPeriod() > 0)
            .collect(Collectors.toList());
    }
}

Это завершает наш обзор наследования в Java. Мы рассмотрели как базовые концепции, так и продвинутые техники использования наследования, включая: - Основы синтаксиса и механики наследования - Абстрактные классы и методы - Порядок инициализации - Работу с коллекциями - Защиту от наследования - Паттерны проектирования - Проблемы и антипаттерны - Практические примеры

пример

[[Programming/java/1. osnovi/Тема 4. Наследование/Урок 1. Наследование/задание|задание]]