JavaRush /Курси /JAVA 25 SELF /Проблеми поліморфізму та абстракцій

Проблеми поліморфізму та абстракцій

JAVA 25 SELF
Рівень 23 , Лекція 3
Відкрита

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: Порушення принципу єдиної відповідальності

Якщо абстракція описує надто багато обов’язків, вона стає важкою в підтримці. Краще розбивати на кілька інтерфейсів або класів.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ