1. Поліморфізм у колекціях: навіщо він потрібен?
Почнімо із запитання: «Навіщо взагалі потрібен поліморфізм у реальних програмах?»
Уявіть зоопарк. У вас є базовий клас Animal, а також багато нащадків: Dog, Cat, Cow, Parrot і навіть Platypus (качкодзьоб, для поціновувачів екзотики). Кожен із них уміє видавати звук (makeSound()), але робить це по‑своєму.
Замість того, щоб створювати окремі масиви для кожного виду тварин, ви оголошуєте масив чи список типу Animal і додаєте до нього будь‑кого:
Animal[] animals = {
new Dog(),
new Cat(),
new Cow(),
new Parrot()
};
Тепер ви можете пройтися цим масивом і викликати makeSound() для кожного:
for (Animal animal : animals) {
animal.makeSound();
}
Магія! Кожен об’єкт сам знає, який звук видавати, і вам не потрібно писати жодних if чи switch.
Аналогія
Уявіть, що ви віддаєте команду «Голос!» групі тварин — кожна сама знає, що робити: собака загавкає, кішка нявкне, а корова — замукає. Ви не уточнюєте, хто є хто — просто викликаєте один і той самий метод.
2. Практичний приклад: ієрархія співробітників
Зробімо приклад ближчим до життя (і до майбутньої роботи в ІТ). Припустімо, маємо компанію з різними співробітниками: менеджери, розробники, тестувальники. Кожен має метод work(), але реалізований він по‑різному.
Оголошення базового класу
public class Employee {
public void work() {
System.out.println("Співробітник працює...");
}
}
Підкласи
public class Manager extends Employee {
@Override
public void work() {
System.out.println("Менеджер проводить нараду.");
}
}
public class Developer extends Employee {
@Override
public void work() {
System.out.println("Розробник пише код.");
}
}
public class Tester extends Employee {
@Override
public void work() {
System.out.println("Тестувальник шукає баги.");
}
}
Використання масиву/списку базового типу
public class CompanyDemo {
public static void main(String[] args) {
Employee[] team = {
new Manager(),
new Developer(),
new Tester(),
new Developer()
};
for (Employee e : team) {
e.work(); // Буде викликано "правильну" версію методу для кожного об'єкта
}
}
}
Результат виконання:
Менеджер проводить нараду.
Розробник пише код.
Тестувальник шукає баги.
Розробник пише код.
У чому переваги?
- Вам не доводиться писати численні перевірки на кшталт «Якщо це Developer — роби те‑то».
- Щоб додати нового співробітника (наприклад, Designer), достатньо створити новий клас і додати його до масиву.
- Код, який використовує масив співробітників, узагалі не змінюється!
3. Переваги поліморфізму: гнучкість і розширюваність
Припустімо, у вашій компанії з’явився новий тип співробітника — Designer. Усе, що вам потрібно зробити, — це створити новий клас:
public class Designer extends Employee {
@Override
public void work() {
System.out.println("Дизайнер малює макети.");
}
}
Тепер можна додати дизайнера до команди:
Employee[] team = {
new Manager(),
new Developer(),
new Tester(),
new Designer()
};
Вуаля! Програма відразу починає коректно працювати з новим типом співробітника, не змінюючи жодного рядка в коді, який перебирає масив і викликає work().
Це і є розширюваність: ваш код легко адаптується до нових типів об’єктів.
4. Обмеження поліморфізму: зворотний бік медалі
На жаль, будь‑яка «магія» має свої обмеження — і свою ціну, як у будь‑якій RPG.
Доступні лише методи базового класу
Коли ви працюєте зі змінною типу Employee, можете викликати лише ті методи, що оголошені в класі Employee. Якщо в класі Developer є спеціальний метод writeCode(), ви не зможете викликати його безпосередньо:
Employee e = new Developer();
// e.writeCode(); // Помилка компіляції: такого методу немає в Employee!
Якщо дуже хочеться викликати спеціальний метод, доведеться виконати приведення типів. Але це крайній випадок. Якщо ви часто виконуєте приведення типів, можливо, варто переглянути проєктування класів — базовий клас або інтерфейс має містити потрібний метод.
if (e instanceof Developer) {
Developer dev = (Developer) e;
dev.writeCode();
}
Та тоді ви втрачаєте універсальність і елегантність, заради яких усе це й починалося. Тому намагайтеся проєктувати базовий клас так, щоб у ньому були лише ті методи, які справді потрібні всім нащадкам.
5. Практика: реалізуємо ієрархію співробітників
Поєднаємо приємне з корисним: напишемо простий застосунок, у якому є кілька типів співробітників, і використаємо поліморфізм для їхнього опрацювання.
Крок 1: Базовий клас і підкласи
// Employee.java
public class Employee {
public void work() {
System.out.println("Співробітник працює...");
}
}
// Manager.java
public class Manager extends Employee {
@Override
public void work() {
System.out.println("Менеджер проводить нараду.");
}
}
// Developer.java
public class Developer extends Employee {
@Override
public void work() {
System.out.println("Розробник пише код.");
}
}
// Tester.java
public class Tester extends Employee {
@Override
public void work() {
System.out.println("Тестувальник шукає баги.");
}
}
Крок 2: Головний клас
// CompanyDemo.java
public class CompanyDemo {
public static void main(String[] args) {
Employee[] team = {
new Manager(),
new Developer(),
new Tester(),
new Developer()
};
for (Employee e : team) {
e.work();
}
}
}
Крок 3: Додамо розширюваність
Припустімо, за місяць у компанії з’являється новий співробітник — дизайнер. Усе, що потрібно:
public class Designer extends Employee {
@Override
public void work() {
System.out.println("Дизайнер малює макети.");
}
}
Готово — тепер можна додати дизайнера до команди:
Employee[] team = {
new Manager(),
new Developer(),
new Tester(),
new Designer()
};
Підсумок
Увесь основний код (CompanyDemo) залишився незмінним! Це і є сила поліморфізму.
6. Типові помилки під час використання поліморфізму
Помилка № 1: Очікування доступу до спеціальних методів через посилання базового типу.
Дуже часто новачки намагаються викликати спеціальні методи підкласу через змінну типу суперкласу. Наприклад:
Employee e = new Developer();
// e.writeCode(); // Помилка! Такий метод не визначено в Employee.
Щоб викликати спеціальний метод, потрібно виконати приведення типів, але так втрачається універсальність.
Помилка № 2: Не використовується анотація @Override.
Якщо її пропустити, можна випадково написати новий метод замість перевизначення (наприклад, помилитися в назві). Тоді поліморфізм просто не спрацює, і буде викликано версію із суперкласу.
Помилка № 3: Відсутність спільного інтерфейсу.
Якщо базовий клас не містить потрібного методу, поліморфізм неможливий. Наприклад, якщо в Employee немає методу work(), то цикл за масивом співробітників не зможе викликати цей метод для всіх.
Помилка № 4: Порушення принципу відкритості/закритості.
Якщо для додавання нового типу співробітника доводиться змінювати код, який перебирає масив/список, — отже, ви не використовуєте поліморфізм належним чином.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ