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 - это ссылка на родительский класс. Используется для:
- Вызова конструктора родителя
- Вызова методов родителя
- Доступа к полям родителя (если скрыты)
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 Наследование - выбор правильного инструмента
Почему это важно:
- Переиспользование кода
- Гибкость через полиморфизм
- Организация классов в иерархии
- Расширяемость программ
Задания для практики
-
Иерархия транспорта:
- Базовый класс Vehicle
- Подклассы: Car, Motorcycle, Truck
- Переопределение методов
- Полиморфный массив
-
Фигуры:
- Базовый класс Shape
- Подклассы: Circle, Rectangle, Triangle
- Метод getArea() в каждом
- Сравнение фигур по площади
-
Сотрудники:
- Базовый Employee
- Подклассы: Manager, Developer, Designer
- Разный расчёт зарплаты
- equals() и hashCode()
Удачи!
Источники: