1. Поліморфізм: що це і навіщо потрібен
Якщо ви думаєте, що поліморфізм — це щось зі світу мутантів Marvel, мушу розчарувати: у програмуванні все значно спокійніше, проте не менш магічно. Поліморфізм — це здатність об’єктів із різною реалізацією реагувати по-різному на однакові виклики методів.
Приклад із життя:
У вас є клас Book і клас Magazine, обидва успадковуються від абстрактного класу LibraryItem. Ви хочете, щоб для будь-якого елемента бібліотеки можна було викликати метод printInfo(), і він виведе потрібну інформацію — для книги це буде автор і назва, для журналу — номер випуску та дата.
Приклад коду:
abstract class LibraryItem {
String title;
LibraryItem(String title) {
this.title = title;
}
abstract void printInfo();
}
class Book extends LibraryItem {
String author;
Book(String title, String author) {
super(title);
this.author = author;
}
@Override
void printInfo() {
System.out.println("Книга: " + title + ", автор: " + author);
}
}
class Magazine extends LibraryItem {
int issueNumber;
Magazine(String title, int issueNumber) {
super(title);
this.issueNumber = issueNumber;
}
@Override
void printInfo() {
System.out.println("Журнал: " + title + ", випуск: " + issueNumber);
}
}
Тепер можна створити масив із різних елементів і викликати printInfo() для кожного:
LibraryItem[] items = {
new Book("Володар мух", "Вільям Голдінг"),
new Magazine("Наука і життя", 5)
};
for (LibraryItem item : items) {
item.printInfo();
}
// Виведе:
// Книга: Володар мух, автор: Вільям Голдінг
// Журнал: Наука і життя, випуск: 5
Ось як працює поліморфізм!
2. Типові помилки з поліморфізмом
Спроба викликати методи, яких немає у базовому типі
Одна з найчастіших помилок — спроба звернутися до методу, який оголошено лише у дочірньому класі, через посилання на базовий тип.
LibraryItem item = new Book("Гаррі Поттер", "Дж. Ролінґ");
// item.getAuthor(); // Помилка компіляції! У LibraryItem немає методу getAuthor()
Компілятор Java перевіряє код на основі того, який тип має змінна (LibraryItem), а не того, який фактичний об’єкт у змінній (Book). Тож якщо вам потрібно викликати метод, специфічний для книги, виконайте приведення типу:
if (item instanceof Book) {
Book book = (Book) item;
// Тепер можна викликати book.getAuthor()
}
Приведення типу без перевірки
Якщо ви впевнені, що об’єкт — це Book, але насправді це не так, отримаєте ClassCastException під час виконання. Наприклад:
LibraryItem item = new Magazine("Forbes", 12);
Book book = (Book) item; // ClassCastException під час виконання
Правильний спосіб — завжди перевіряти тип:
if (item instanceof Book) {
Book book = (Book) item;
// Гаразд
} else {
System.out.println("Це не книга!");
}
Ігнорування переваг поліморфізму
Іноді розробники пишуть код так, що він жорстко прив’язаний до конкретних типів, хоча можна було б використати абстракції. Наприклад, якщо ви пишете:
Book[] books = ...;
for (Book book : books) {
book.printInfo();
}
Це працює лише для книг. А якщо завтра з’являться журнали, газети, комікси? Краще використовувати масив LibraryItem[] і працювати з методами базового класу або інтерфейсу.
3. Абстракції: навіщо вони потрібні й як їх не зіпсувати
Абстрактні класи та інтерфейси
Абстракція — це мистецтво виокремлювати головне та приховувати деталі. У Java для цього є абстрактні класи та інтерфейси.
- Абстрактний клас — це клас, який не можна створити напряму, його можна лише наслідувати.
- Інтерфейс — це контракт: що має вміти клас, але не те, як він це робить.
Помилка 1: Створення абстрактного класу без абстрактних методів
Якщо у вашому абстрактному класі немає жодного абстрактного методу, замисліться — а чи справді він має бути абстрактним? Можливо, простіше зробити звичайний клас?
abstract class UselessAbstract {
void sayHello() {
System.out.println("Hello!");
}
}
// Краще зробити звичайний клас, якщо немає абстрактних методів
Помилка 2: Відсутність реалізації обов’язкових методів у нащадках
Якщо клас успадковує абстрактний клас або реалізує інтерфейс, він зобов’язаний реалізувати всі абстрактні методи. Якщо забути — компілятор нагадає, але іноді методи реалізують «для галочки», і вони нічого не роблять. Це погано для підтримки коду.
class Magazine extends LibraryItem {
Magazine(String title, int issueNumber) {
super(title);
// ...
}
@Override
void printInfo() {
// Порожньо — погано.
}
}
Помилка 3: Надто глибока або заплутана ієрархія абстракцій
Коли класи успадковуються один від одного на п’ять–десять рівнів, розібратися в них стає дуже складно. Краще робити «плоскі» ієрархії, де все зрозуміло.
Поганий приклад:
LibraryItem
|
BookItem
|
PrintedBook
|
IllustratedBook
|
ChildrenIllustratedBook
Складно, чи не так? Краще обмежитися двома–трьома рівнями.
4. Практика: застосування поліморфізму та абстракцій у навчальному застосунку
Доопрацюймо ваш навчальний застосунок для бібліотеки. Раніше у вас були лише книги. Тепер додамо журнали та реалізуємо спільний інтерфейс для друкованих видань.
Оголосимо абстрактний клас:
abstract class LibraryItem {
protected String title;
public LibraryItem(String title) {
this.title = title;
}
public abstract void printInfo();
}
Додамо дочірні класи:
class Book extends LibraryItem {
private String author;
public Book(String title, String author) {
super(title);
this.author = author;
}
@Override
public void printInfo() {
System.out.println("Книга: " + title + ", автор: " + author);
}
}
class Magazine extends LibraryItem {
private int issueNumber;
public Magazine(String title, int issueNumber) {
super(title);
this.issueNumber = issueNumber;
}
@Override
public void printInfo() {
System.out.println("Журнал: " + title + ", випуск: " + issueNumber);
}
}
Використаємо поліморфізм:
LibraryItem[] items = {
new Book("Чистий код", "Роберт Мартін"),
new Magazine("Java World", 3)
};
for (LibraryItem item : items) {
item.printInfo();
}
Додамо інтерфейс для електронних видань
Припустімо, деякі видання можна читати онлайн. Уведемо інтерфейс:
interface ReadableOnline {
void openOnline();
}
class EBook extends Book implements ReadableOnline {
private String url;
public EBook(String title, String author, String url) {
super(title, author);
this.url = url;
}
@Override
public void openOnline() {
System.out.println("Відкриваємо електронну книгу за адресою: " + url);
}
}
Тепер можна працювати з електронними книгами через інтерфейс:
ReadableOnline ebook = new EBook("Java для чайників", "Баррі Берд", "https://example.com/java");
ebook.openOnline();
5. Як уникнути проблем із поліморфізмом та абстракціями: найкращі практики
- Використовуйте інтерфейси та абстрактні класи для опису поведінки, а не стану.
Наприклад, інтерфейс Printable добре описує можливість «друку», а от зберігати в інтерфейсі поле String title — уже погана ідея. - Перевіряйте тип об’єкта за допомогою instanceof перед приведенням.
Особливо якщо об’єкт може бути різного типу. Це вбереже від ClassCastException. - Прагніть до «плоских» і зрозумілих ієрархій.
Чим простіше дерево успадкування — тим легше підтримувати та розширювати код. - Намагайтеся не робити «беззмістовних» абстракцій.
Якщо клас не містить абстрактних методів і не призначений для наслідування — не робіть його абстрактним. - Використовуйте анотацію @Override завжди, коли перевизначаєте метод.
Це допомагає компілятору спіймати помилки в сигнатурі.
6. Типові помилки під час роботи з поліморфізмом та абстракціями
Помилка № 1: Приведення типу без перевірки
Іноді спокусливо «зрізати кути» і привести тип без перевірки. Це може спрацювати, а може призвести до раптового падіння програми. Завжди використовуйте instanceof:
if (item instanceof Book) {
Book book = (Book) item;
// ...
}
Помилка № 2: Спроба викликати метод дочірнього класу через посилання на базовий тип
LibraryItem item = new Book("Java", "Автор");
item.getAuthor(); // Помилка компіляції: у LibraryItem немає такого методу!
Рішення — або виконати приведення типу, або додати потрібний метод у базовий клас (якщо це виправдано).
Помилка № 3: Неповна реалізація інтерфейсу або абстрактного класу
Якщо забути реалізувати всі методи інтерфейсу — компілятор не дасть зібрати проєкт. Але якщо реалізувати «заглушки», які нічого не роблять, це призведе до неочікуваної поведінки.
Помилка № 4: Надто глибока ієрархія успадкування
Якщо у вас більше трьох рівнів успадкування — замисліться, чи не можна спростити архітектуру.
Помилка № 5: Порушення принципу єдиної відповідальності
Якщо абстракція описує надто багато обов’язків, вона стає важкою в підтримці. Краще розбивати на кілька інтерфейсів або класів.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ