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. Как избежать проблем с полиморфизмом и абстракциями: best practices

  • Используйте интерфейсы и абстрактные классы для описания поведения, а не состояния.
    Например, интерфейс 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: Нарушение принципа единой ответственности

Если абстракция описывает слишком много обязанностей, она становится трудноподдерживаемой. Лучше разбивать на несколько интерфейсов или классов.

1
Задача
JAVA 25 SELF, 23 уровень, 3 лекция
Недоступна
Звуки животных: Когда собака гавкает 🐕
Звуки животных: Когда собака гавкает 🐕
1
Задача
JAVA 25 SELF, 23 уровень, 3 лекция
Недоступна
Виртуальный холст: Разнообразие фигур 🎨
Виртуальный холст: Разнообразие фигур 🎨
1
Задача
JAVA 25 SELF, 23 уровень, 3 лекция
Недоступна
Умный дом: Кипящий чайник 💡
Умный дом: Кипящий чайник 💡
1
Задача
JAVA 25 SELF, 23 уровень, 3 лекция
Недоступна
IT-Компания: Программист, который работает и отчитывается 👨‍💻
IT-Компания: Программист, который работает и отчитывается 👨‍💻
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ