1. Вступ
Поліморфізм — це одна з трьох основних концепцій обʼєктно-орієнтованого програмування (поруч із наслідуванням та інкапсуляцією). Дослівно слово походить із грецької: «poly» — «багато», «morph» — «форма». У програмуванні це означає: один інтерфейс — багато реалізацій.
Визначення
Поліморфізм — це здатність обʼєктів різних класів реагувати на однакові повідомлення (виклики методів) по-різному.
Тобто, якщо у вас є метод makeSound(), ви можете викликати його для будь-якої тварини, але кішка нявкне, собака гавкне, а корова замукає. Для програміста це просто виклик animal.makeSound(), а що станеться насправді — залежить від того, який саме обʼєкт стоїть за цією змінною.
Аналогія з життя
Уявіть, що у вас удома є пульт від телевізора, і цим же пультом можна керувати колонками, проєктором і навіть кавомашиною. Ви натискаєте кнопку «Увімкнути» — turnOn(), і кожен пристрій реагує по-своєму. Головне — у всіх є «кнопка» ввімкнення, але реалізація різна.
Приклад на Java
class Animal {
void makeSound() {
System.out.println("Якийсь звук...");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Гав!");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Няв!");
}
}
class Cow extends Animal {
@Override
void makeSound() {
System.out.println("Мууу!");
}
}
Тепер ми можемо зробити так:
Animal animal1 = new Dog();
Animal animal2 = new Cat();
Animal animal3 = new Cow();
animal1.makeSound(); // Гав!
animal2.makeSound(); // Няв!
animal3.makeSound(); // Мууу!
Зверніть увагу: усі змінні мають тип Animal, але результат виклику залежить від фактичного типу обʼєкта.
2. Види поліморфізму
У Java (і в більшості мов ООП) розрізняють два основних види поліморфізму:
Компіляторний (статичний) поліморфізм — перевантаження методів (overloading)
Так називають випадок, коли в одному класі є кілька методів з однаковим іменем, але різними параметрами. Компілятор сам вирішує, який метод викликати, залежно від переданих аргументів.
Приклад (зазирнемо вперед — детальніше у наступній лекції):
class Printer {
void print(int x) {
System.out.println("Число: " + x);
}
void print(String s) {
System.out.println("Рядок: " + s);
}
}
Під час виконання (динамічний) поліморфізм — перевизначення методів (overriding)
Це коли метод визначено в базовому класі, а потім перевизначено в підкласах. Який саме метод буде викликано — вирішується під час виконання програми (runtime), залежно від фактичного типу обʼєкта.
Приклад — див. вище з тваринами.
3. Навіщо потрібен поліморфізм?
Поліморфізм — це не просто гарне слово для співбесіди. Це інструмент, який робить ваш код гнучким, розширюваним і зручним для підтримки.
Універсальність коду
Ви можете писати код, який працює з обʼєктами базового типу, не переймаючись деталями їх реалізації. Наприклад, якщо у вас є список тварин, ви можете пройтися ним і викликати в кожної makeSound(), не замислюючись, кішка це чи собака.
Animal[] animals = { new Dog(), new Cat(), new Cow() };
for (Animal animal : animals) {
animal.makeSound(); // Кожного разу буде викликано "правильний" метод
}
Легкість розширення
Якщо завтра керівник скаже: «Додаймо папугу!», ви просто пишете новий клас Parrot extends Animal і додаєте його в масив. Весь інший код залишиться незмінним. Це — відкритість для розширення й закритість для змін (принцип OCP із SOLID).
Спрощення архітектури
Ви можете будувати складні системи, де окремі частини взаємодіють між собою через абстракції (базові класи або інтерфейси), не переймаючись конкретними реалізаціями. Це заощаджує час, нерви й каву.
4. Ключові поняття: посилальний і фактичний тип
Посилальний тип змінної
Коли ви пишете Animal animal = new Dog();, змінна animal має посилальний тип Animal, тобто компілятор «вважає», що це тварина, і дозволяє тільки ті методи, які оголошені в класі Animal.
Фактичний (реальний) тип обʼєкта
Але в памʼяті фактично зберігається обʼєкт типу Dog. Саме він визначає, який метод буде викликано під час звернення до makeSound().
Ілюстрація
Animal animal = new Dog();
animal.makeSound(); // Викличе Dog.makeSound(), а не Animal.makeSound()
Важливо! Через посилання базового типу (Animal) ви не зможете викликати методи, які є тільки у Dog, якщо вони не оголошені в базовому класі.
Пізнє (динамічне) звʼязування
Це «магія», яка відбувається під час виконання: коли ви викликаєте метод через посилання базового типу, JVM дивиться на фактичний тип обʼєкта і викликає «правильну» реалізацію. Це і є поліморфізм у дії.
5. Практичний приклад: поліморфізм у застосунку
Продовжімо розвиток нашого навчального застосунку. Припустімо, ми пишемо просту симуляцію зоопарку. У нас є базовий клас Animal і кілька його нащадків. Ми хочемо, щоб усі тварини могли «видавати звук», але не хочемо щоразу писати окремий код для кожного типу тварини.
Крок 1: Базовий клас і нащадки
class Animal {
void makeSound() {
System.out.println("Якийсь звук...");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Гав!");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Няв!");
}
}
Крок 2: Масив тварин
Animal[] zoo = { new Dog(), new Cat(), new Animal() };
Крок 3: Перебирання та виклик методу
for (Animal animal : zoo) {
animal.makeSound();
}
Результат виконання:
Гав!
Няв!
Якийсь звук...
Зверніть увагу: ми не знаємо наперед, хто саме перебуває в масиві — але програма сама розбирається і викликає потрібний метод.
6. Схематичне зображення поліморфізму
. Animal (makeSound)
/ \
Dog Cat
(makeSound) (makeSound)
Animal animal = new Dog();
animal.makeSound(); // --> Dog.makeSound()
Animal animal = new Cat();
animal.makeSound(); // --> Cat.makeSound()
7. Ще один приклад: поліморфізм у реальному завданні
Припустімо, ви пишете програму для керування співробітниками компанії. У вас є базовий клас Employee і два нащадки: Manager і Developer. Усі співробітники можуть працювати — work(), але роблять це по-різному.
class Employee {
void work() {
System.out.println("Співробітник працює.");
}
}
class Manager extends Employee {
@Override
void work() {
System.out.println("Менеджер проводить нараду.");
}
}
class Developer extends Employee {
@Override
void work() {
System.out.println("Розробник пише код.");
}
}
Тепер ви можете зробити так:
Employee[] staff = { new Manager(), new Developer(), new Employee() };
for (Employee emp : staff) {
emp.work();
}
Результат:
Менеджер проводить нараду.
Розробник пише код.
Співробітник працює.
8. Коли поліморфізм не працює
Поліморфізм працює лише для методів, оголошених у базовому класі. Якщо в підкласі є свій унікальний метод, через посилання базового типу ви його не побачите.
class Dog extends Animal {
void fetchStick() {
System.out.println("Собака приносить палицю!");
}
}
Animal animal = new Dog();
// animal.fetchStick(); // Помилка компіляції! Такий метод не видно через Animal
Щоб викликати специфічний метод, слід виконати приведення до потрібного типу:
if (animal instanceof Dog) {
((Dog) animal).fetchStick();
}
Але це вже інша історія — головне: через поліморфізм доступні лише методи, оголошені в базовому класі.
9. Типові помилки під час роботи з поліморфізмом
Помилка № 1: Очікування, що через посилання базового типу будуть доступні всі методи підкласу. Насправді доступні тільки ті, що оголошені в базовому класі.
Помилка № 2: Невикористання анотації @Override під час перевизначення методу. Без неї можна випадково написати метод із неправильною сигнатурою, і тоді поліморфізм не спрацює (метод базового класу не буде перевизначений).
Помилка № 3: Спроба викликати специфічний метод підкласу без приведення типу. Компілятор цього не дозволить, адже він не знає, хто саме «сидить» за посиланням базового типу.
Помилка № 4: Плутанина між перевантаженням (overloading) і перевизначенням (overriding). Перевантаження — це кілька методів з одним імʼям і різними параметрами в одному класі. Перевизначення — це зміна поведінки методу в підкласі.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ