Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

1.9. Наследование и полиморфизм

Материалы

ТипСсылка
Документссылка
Видеоссылка

Заметки

Добро пожаловать во вторую часть изучения ООП! Здесь мы освоим два мощнейших инструмента: наследование для переиспользования кода и полиморфизм для гибкости программ.

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

Наследование (Inheritance) - это механизм, позволяющий создавать новые классы на основе существующих, переиспользуя их код.

Аналогия из жизни

Думайте о наследовании как о генетике:

  • Дети наследуют черты от родителей
  • Но при этом имеют свои уникальные особенности
Родитель (Animal)
├── глаза
├── уши
└── может дышать

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

Ребёнок (Dog)
├── глаза (от родителя)
├── уши (от родителя)
├── может дышать (от родителя)
└── может лаять (своё!)

Зачем нужно наследование?

1. Переиспользование кода (Code Reuse) Не пишем один и тот же код многократно.

2. Расширение функциональности Добавляем новые возможности к существующим классам.

3. Иерархия и организация Создаём логическую структуру классов.

4. Полиморфизм Позволяет работать с объектами через общий интерфейс.

Базовое наследование

Синтаксис

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

class Потомок extends Родитель {
    // поля и методы потомка
    // + унаследованные от родителя
}

Первый пример

// Базовый класс (родитель, суперкласс, parent class)
public class Animal {
    protected String name;  // protected - доступно наследникам
    protected int age;
    
    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("Создано животное: " + name);
    }
    
    public void eat() {
        System.out.println(name + " ест");
    }
    
    public void sleep() {
        System.out.println(name + " спит");
    }
    
    public void breathe() {
        System.out.println(name + " дышит");
    }
    
    public void makeSound() {
        System.out.println(name + " издаёт звук");
    }
}

// Производный класс (потомок, подкласс, child class)
public class Dog extends Animal {
    private String breed;
    
    public Dog(String name, int age, String breed) {
        super(name, age);  // вызов конструктора родителя
        this.breed = breed;
        System.out.println("Создана собака породы: " + breed);
    }
    
    // Новые методы - только у Dog
    public void bark() {
        System.out.println(name + " лает: Гав-гав!");
    }
    
    public void fetch() {
        System.out.println(name + " приносит палку");
    }
    
    // Переопределяем метод родителя
    @Override
    public void makeSound() {
        System.out.println(name + " лает громко!");
    }
}

// Ещё один потомок
public class Cat extends Animal {
    public Cat(String name, int age) {
        super(name, age);
        System.out.println("Создан кот");
    }
    
    public void meow() {
        System.out.println(name + " мяукает: Мяу!");
    }
    
    public void scratch() {
        System.out.println(name + " точит когти");
    }
    
    @Override
    public void makeSound() {
        System.out.println(name + " мяукает нежно!");
    }
}

Использование

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog("Шарик", 3, "Лабрадор");
        Cat cat = new Cat("Мурка", 2);
        
        System.out.println("\n=== Собака ===");
        // Унаследованные методы от Animal
        dog.eat();      // Шарик ест
        dog.sleep();    // Шарик спит
        dog.breathe();  // Шарик дышит
        
        // Собственные методы Dog
        dog.bark();     // Шарик лает: Гав-гав!
        dog.fetch();    // Шарик приносит палку
        
        // Переопределённый метод
        dog.makeSound();  // Шарик лает громко!
        
        System.out.println("\n=== Кошка ===");
        cat.eat();        // Мурка ест
        cat.meow();       // Мурка мяукает: Мяу!
        cat.scratch();    // Мурка точит когти
        cat.makeSound();  // Мурка мяукает нежно!
    }
}

Вывод:

Создано животное: Шарик
Создана собака породы: Лабрадор
Создано животное: Мурка
Создан кот

=== Собака ===
Шарик ест
Шарик спит
Шарик дышит
Шарик лает: Гав-гав!
Шарик приносит палку
Шарик лает громко!

=== Кошка ===
Мурка ест
Мурка мяукает: Мяу!
Мурка точит когти
Мурка мяукает нежно!

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

super - это ссылка на родительский класс. Используется для:

  1. Вызова конструктора родителя
  2. Вызова методов родителя
  3. Доступа к полям родителя (если скрыты)

super() - вызов конструктора родителя

public class Animal {
    protected String name;
    
    public Animal(String name) {
        this.name = name;
        System.out.println("Animal constructor: " + name);
    }
}

public class Dog extends Animal {
    private String breed;
    
    public Dog(String name, String breed) {
        super(name);  // ОБЯЗАТЕЛЬНО вызываем конструктор родителя
        this.breed = breed;
        System.out.println("Dog constructor: " + breed);
    }
}

Правила super():

  • ⚠️ Должен быть ПЕРВОЙ строкой в конструкторе
  • ⚠️ Если не вызвать явно, Java вызовет super() автоматически
  • ⚠️ Если у родителя нет конструктора без параметров - ОБЯЗАТЕЛЬНО явный вызов super()

super для вызова методов родителя

public class Animal {
    public void eat() {
        System.out.println("Животное ест");
    }
}

public class Dog extends Animal {
    @Override
    public void eat() {
        System.out.println("Собака готовится к еде...");
        super.eat();  // вызываем метод родителя
        System.out.println("Собака наелась!");
    }
}

Dog dog = new Dog();
dog.eat();
// Вывод:
// Собака готовится к еде...
// Животное ест
// Собака наелась!

super для доступа к полям родителя

public class Parent {
    protected int value = 10;
}

public class Child extends Parent {
    private int value = 20;  // скрывает поле родителя
    
    public void display() {
        System.out.println("Child value: " + value);        // 20
        System.out.println("Child value: " + this.value);   // 20
        System.out.println("Parent value: " + super.value); // 10
    }
}

Иерархия наследования

Классы могут образовывать дерево наследования:

           Animal
          /      \
       Dog       Cat
        |         |
     Puppy    Kitten
public class Animal {
    protected String name;
    
    public Animal(String name) {
        this.name = name;
    }
    
    public void makeSound() {
        System.out.println("Животное издаёт звук");
    }
}

public class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }
    
    @Override
    public void makeSound() {
        System.out.println(name + " лает");
    }
    
    public void bark() {
        System.out.println(name + " гавкает!");
    }
}

public class Puppy extends Dog {
    private int monthsOld;
    
    public Puppy(String name, int monthsOld) {
        super(name);
        this.monthsOld = monthsOld;
    }
    
    @Override
    public void makeSound() {
        System.out.println(name + " тявкает по-щенячьи");
    }
    
    public void play() {
        System.out.println(name + " играет с игрушкой");
    }
}

// Использование
Puppy puppy = new Puppy("Малыш", 3);
puppy.play();       // из Puppy
puppy.bark();       // из Dog
puppy.makeSound();  // из Puppy (переопределён)
// puppy наследует всё от Dog, а Dog - от Animal

Цепочка вызова конструкторов

При создании объекта конструкторы вызываются от корня к листу:

Puppy puppy = new Puppy("Малыш", 3);
// 1. Вызывается Animal("Малыш")
// 2. Вызывается Dog("Малыш")
// 3. Вызывается Puppy("Малыш", 3)

Правила и ограничения наследования

1. Java = Одиночное наследование

Java НЕ поддерживает множественное наследование классов:

// ❌ ОШИБКА! Нельзя наследовать от двух классов
class C extends A, B {  // ОШИБКА КОМПИЛЯЦИИ!
}

Но можно реализовать несколько интерфейсов (об этом в следующей главе):

// ✅ OK! Можно реализовать много интерфейсов
class C extends A implements B, D, E {
}

2. Все классы наследуются от Object

public class Dog {
    // неявно: extends Object
}

// Эквивалентно:
public class Dog extends Object {
}

Object - корень всей иерархии Java. Поэтому у ВСЕХ объектов есть методы:

  • toString()
  • equals()
  • hashCode()
  • getClass()
  • и другие

3. private члены НЕ наследуются

public class Parent {
    private int secret = 42;
    
    public int getSecret() {
        return secret;
    }
}

public class Child extends Parent {
    public void test() {
        // System.out.println(secret);  // ОШИБКА! private не наследуется
        System.out.println(getSecret());  // OK через public метод
    }
}

4. Конструкторы НЕ наследуются

public class Parent {
    public Parent(int x) {
        System.out.println("Parent: " + x);
    }
}

public class Child extends Parent {
    // Конструктор Parent(int) не наследуется!
    // Должны создать свой конструктор
    
    public Child(int x) {
        super(x);  // явный вызов родительского
    }
}

// Child child = new Child();  // ОШИБКА! Нет конструктора без параметров
Child child = new Child(10);   // OK

5. final класс нельзя наследовать

public final class String {
    // ...
}

// public class MyString extends String {}  // ОШИБКА!

6. Циклическое наследование запрещено

// ❌ ОШИБКА!
class A extends B {}
class B extends A {}  // циклическое наследование!

Переопределение методов (Method Overriding)

Переопределение - это создание в подклассе метода с той же сигнатурой, что и в родителе, но с другой реализацией.

Базовый пример

public class Animal {
    public void makeSound() {
        System.out.println("Животное издаёт звук");
    }
}

public class Dog extends Animal {
    @Override  // рекомендуется использовать!
    public void makeSound() {
        System.out.println("Собака лает!");
    }
}

public class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Кошка мяукает!");
    }
}

Animal animal = new Animal();
Animal dog = new Dog();
Animal cat = new Cat();

animal.makeSound();  // Животное издаёт звук
dog.makeSound();     // Собака лает!
cat.makeSound();     // Кошка мяукает!

Аннотация @Override

@Override - это аннотация, которая говорит компилятору: “Я переопределяю метод родителя”.

Зачем нужна:

  • ✅ Компилятор проверит корректность переопределения
  • ✅ Защита от опечаток
  • ✅ Улучшает читаемость кода
public class Dog extends Animal {
    @Override
    public void makeSound() {  // OK
        System.out.println("Гав!");
    }
    
    // @Override
    // public void makeSond() {  // ОШИБКА! Опечатка, нет такого метода в родителе
    //     System.out.println("Гав!");
    // }
}

Правила переопределения

1. Сигнатура должна совпадать

// Родитель
public void method(int x) { }

// ✅ OK - точно такая же сигнатура
@Override
public void method(int x) { }

// ❌ ОШИБКА - другая сигнатура (это перегрузка, не переопределение!)
public void method(double x) { }

2. Возвращаемый тип должен быть тем же или подтипом (covariant return)

public class Parent {
    public Number getValue() {
        return 42;
    }
}

public class Child extends Parent {
    @Override
    public Integer getValue() {  // Integer - подтип Number, OK!
        return 100;
    }
}

3. Модификатор доступа не может быть более строгим

public class Parent {
    protected void method() { }
}

public class Child extends Parent {
    // ✅ OK - можем расширить доступ
    @Override
    public void method() { }
    
    // ❌ ОШИБКА - нельзя сузить доступ
    // @Override
    // private void method() { }
}

4. Нельзя переопределить final метод

public class Parent {
    public final void method() {  // final - нельзя переопределить
        System.out.println("Parent");
    }
}

public class Child extends Parent {
    // @Override
    // public void method() {  // ОШИБКА! Метод final
    //     System.out.println("Child");
    // }
}

5. Нельзя переопределить static метод

Static методы скрываются (hiding), а не переопределяются:

public class Parent {
    public static void staticMethod() {
        System.out.println("Parent static");
    }
}

public class Child extends Parent {
    // Это скрытие (hiding), не переопределение!
    public static void staticMethod() {
        System.out.println("Child static");
    }
}

Parent.staticMethod();  // Parent static
Child.staticMethod();   // Child static

Parent p = new Child();
p.staticMethod();       // Parent static (!)
// Зависит от типа ПЕРЕМЕННОЙ, не объекта

Полиморфизм

Полиморфизм (Polymorphism) - это способность объектов разных классов отвечать на одни и те же вызовы по-своему.

Буквально: “много форм” (poly = много, morph = форма)

Суть полиморфизма

Animal animal1 = new Dog("Шарик", 3);
Animal animal2 = new Cat("Мурка", 2);
Animal animal3 = new Animal("Попугай", 1);

// Один и тот же вызов - разное поведение!
animal1.makeSound();  // лает
animal2.makeSound();  // мяукает
animal3.makeSound();  // издаёт звук

// Тип переменной: Animal
// Тип объекта: Dog, Cat, Animal
// Вызывается метод РЕАЛЬНОГО объекта, не типа переменной!

Зачем нужен полиморфизм?

1. Универсальный код

public void feedAnimal(Animal animal) {
    animal.eat();  // работает для любого животного!
}

feedAnimal(new Dog("Шарик", 3));
feedAnimal(new Cat("Мурка", 2));
feedAnimal(new Bird("Кеша", 1));

2. Массивы и коллекции разных типов

Animal[] zoo = {
    new Dog("Рекс", 4),
    new Cat("Барсик", 3),
    new Dog("Тузик", 2),
    new Cat("Мурка", 5)
};

// Кормим всех одинаково!
for (Animal animal : zoo) {
    animal.eat();
    animal.makeSound();
}

3. Гибкость и расширяемость

Добавляем новый класс - старый код продолжает работать:

// Старый код
public class Zoo {
    public void feedAll(Animal[] animals) {
        for (Animal animal : animals) {
            animal.eat();
        }
    }
}

// Добавляем новый класс
public class Bird extends Animal {
    // ...
}

// Старый код feedAll() работает и с Bird без изменений!

Практический пример: зоопарк

public class Zoo {
    private Animal[] animals;
    
    public Zoo(Animal[] animals) {
        this.animals = animals;
    }
    
    public void feedingTime() {
        System.out.println("=== Время кормления! ===");
        for (Animal animal : animals) {
            System.out.println("\nКормим " + animal.name + ":");
            animal.eat();
            animal.makeSound();
        }
    }
    
    public void nightTime() {
        System.out.println("\n=== Наступила ночь ===");
        for (Animal animal : animals) {
            animal.sleep();
        }
    }
}

// Использование
Animal[] animals = {
    new Dog("Рекс", 4, "Овчарка"),
    new Cat("Барсик", 3),
    new Dog("Тузик", 2, "Бульдог"),
    new Cat("Мурка", 5),
    new Animal("Попугай", 2)
};

Zoo zoo = new Zoo(animals);
zoo.feedingTime();
zoo.nightTime();

Приведение типов и instanceof

Восходящее приведение (Upcasting)

Автоматическое, безопасное:

Dog dog = new Dog("Шарик", 3);
Animal animal = dog;  // автоматическое приведение Dog → Animal
animal.eat();         // OK
// animal.bark();     // ОШИБКА! Animal не знает про bark()

Нисходящее приведение (Downcasting)

Явное, может быть небезопасным:

Animal animal = new Dog("Шарик", 3);

// Явное приведение
Dog dog = (Dog) animal;  // OK, потому что объект действительно Dog
dog.bark();              // теперь можем вызвать bark()

// Опасно!
Cat cat = (Cat) animal;  // ClassCastException в runtime!

instanceof - проверка типа

Безопасная проверка перед приведением:

Animal animal = new Dog("Шарик", 3);

// Старый способ
if (animal instanceof Dog) {
    Dog dog = (Dog) animal;
    dog.bark();
}

if (animal instanceof Cat) {
    Cat cat = (Cat) animal;
    cat.meow();
}

Pattern Matching (Java 16+)

Более элегантный способ:

Animal animal = new Dog("Шарик", 3);

// Проверка и приведение в одной строке!
if (animal instanceof Dog dog) {
    dog.bark();  // dog уже приведён к типу Dog
}

if (animal instanceof Cat cat) {
    cat.meow();
} else {
    System.out.println("Это не кот");
}

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

public class AnimalProcessor {
    public static void processAnimal(Animal animal) {
        // Общие действия для всех
        animal.eat();
        animal.makeSound();
        
        // Специфичные действия
        if (animal instanceof Dog dog) {
            dog.fetch();
            dog.bark();
        } else if (animal instanceof Cat cat) {
            cat.scratch();
            cat.meow();
        } else {
            System.out.println("Неизвестное животное");
        }
    }
    
    public static void main(String[] args) {
        processAnimal(new Dog("Рекс", 4, "Овчарка"));
        processAnimal(new Cat("Мурка", 3));
        processAnimal(new Animal("Попугай", 2));
    }
}

Класс Object - корень иерархии

Все классы в Java неявно наследуются от Object:

public class Dog {
    // неявно extends Object
}

Важные методы Object

1. toString()

Возвращает строковое представление объекта.

public class Dog {
    private String name;
    private int age;
    
    public Dog(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    @Override
    public String toString() {
        return "Dog{name='" + name + "', age=" + age + "}";
    }
}

Dog dog = new Dog("Шарик", 3);
System.out.println(dog);  // Dog{name='Шарик', age=3}

// Без @Override было бы:
// Dog@15db9742 (имя класса @ хеш-код)

2. equals()

Сравнивает содержимое объектов.

public class Dog {
    private String name;
    private int age;
    
    // ... конструктор ...
    
    @Override
    public boolean equals(Object obj) {
        // 1. Проверка на тот же объект
        if (this == obj) return true;
        
        // 2. Проверка на null
        if (obj == null) return false;
        
        // 3. Проверка типа
        if (getClass() != obj.getClass()) return false;
        
        // 4. Сравнение полей
        Dog other = (Dog) obj;
        return age == other.age && 
               Objects.equals(name, other.name);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

Dog dog1 = new Dog("Шарик", 3);
Dog dog2 = new Dog("Шарик", 3);
Dog dog3 = new Dog("Бобик", 5);

System.out.println(dog1.equals(dog2));  // true
System.out.println(dog1.equals(dog3));  // false
System.out.println(dog1 == dog2);       // false (разные объекты!)

Контракт equals():

  • Рефлексивность: x.equals(x) должно быть true
  • Симметричность: если x.equals(y), то y.equals(x)
  • Транзитивность: если x.equals(y) и y.equals(z), то x.equals(z)
  • Консистентность: многократные вызовы дают одинаковый результат
  • x.equals(null) всегда false

3. hashCode()

Возвращает хеш-код объекта (для хеш-таблиц).

Важное правило: Если переопределяете equals(), ОБЯЗАТЕЛЬНО переопределите hashCode()!

@Override
public int hashCode() {
    return Objects.hash(name, age);
}

4. getClass()

Возвращает класс объекта:

Dog dog = new Dog("Шарик", 3);
Class<?> clazz = dog.getClass();

System.out.println(clazz.getName());        // Dog
System.out.println(clazz.getSimpleName());  // Dog

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

IS-A vs HAS-A

Наследование (IS-A): Собака ЯВЛЯЕТСЯ животным

class Dog extends Animal {
    // Dog IS-A Animal
}

Композиция (HAS-A): Машина ИМЕЕТ двигатель

class Car {
    private Engine engine;  // Car HAS-A Engine
}

Когда использовать наследование?

Используйте наследование если:

  • Подкласс действительно является специализацией суперкласса
  • Нужно переопределить поведение
  • Отношение “IS-A” логично
  • Классы тесно связаны
// ✅ Хорошо - Dog IS-A Animal
class Dog extends Animal { }

// ✅ Хорошо - ArrayList IS-A List
class ArrayList extends AbstractList { }

НЕ используйте наследование если:

  • Просто хотите переиспользовать код
  • Отношение “HAS-A” больше подходит
  • Нарушается принцип подстановки Лисков
// ❌ Плохо - Stack is not really an ArrayList
class Stack extends ArrayList {
    // Наследует много ненужных методов
}

// ✅ Лучше - композиция
class Stack {
    private List<Object> elements = new ArrayList<>();
    
    public void push(Object item) {
        elements.add(item);
    }
    
    public Object pop() {
        return elements.remove(elements.size() - 1);
    }
}

Проблемы наследования

1. Хрупкость базового класса

Изменения в родителе могут сломать потомков.

2. Жёсткая связанность

Потомок сильно зависит от родителя.

3. Нарушение инкапсуляции

Потомок должен знать детали реализации родителя.

Преимущества композиции

1. Гибкость

class Car {
    private Engine engine;
    
    // Можем легко заменить двигатель!
    public void setEngine(Engine engine) {
        this.engine = engine;
    }
}

2. Слабая связанность

3. Следование принципу “Program to interface”

Правило

“Предпочитайте композицию наследованию”

— Joshua Bloch, “Effective Java”

Но это не значит “никогда не используйте наследование”! Используйте наследование для истинных IS-A отношений.

Лучшие практики

1. Используйте @Override

@Override
public void method() {
    // компилятор проверит корректность
}

2. Делайте методы final если не планируете переопределение

public final void criticalMethod() {
    // нельзя переопределить
}

3. Делайте классы final или проектируйте для наследования

// Либо запрещаем наследование
public final class ImmutableClass { }

// Либо документируем и проектируем для расширения
public class ExtensibleClass {
    /**
     * Hook method for subclasses
     */
    protected void hook() { }
}

4. Не вызывайте переопределяемые методы в конструкторе

// ❌ ОПАСНО!
public class Parent {
    public Parent() {
        init();  // переопределяемый метод!
    }
    
    public void init() { }
}

public class Child extends Parent {
    private String data = "initialized";
    
    @Override
    public void init() {
        System.out.println(data);  // может быть null!
    }
}

5. Используйте protected для методов расширения

public class Base {
    protected void extensionPoint() {
        // для переопределения в потомках
    }
}

Итоги

Вы освоили наследование и полиморфизм!

Ключевые концепции:

  • Наследование - переиспользование кода через extends
  • super - доступ к родителю
  • Переопределение - изменение поведения в потомках
  • Полиморфизм - один интерфейс, много реализаций
  • instanceof - проверка типа
  • Object - корень всей иерархии
  • Композиция vs Наследование - выбор правильного инструмента

Почему это важно:

  • Переиспользование кода
  • Гибкость через полиморфизм
  • Организация классов в иерархии
  • Расширяемость программ

Задания для практики

  1. Иерархия транспорта:

    • Базовый класс Vehicle
    • Подклассы: Car, Motorcycle, Truck
    • Переопределение методов
    • Полиморфный массив
  2. Фигуры:

    • Базовый класс Shape
    • Подклассы: Circle, Rectangle, Triangle
    • Метод getArea() в каждом
    • Сравнение фигур по площади
  3. Сотрудники:

    • Базовый Employee
    • Подклассы: Manager, Developer, Designer
    • Разный расчёт зарплаты
    • equals() и hashCode()

Удачи!


Источники: