CodeGym /Kursy /JAVA 25 SELF /Problemy polimorfizmu i abstrakcji

Problemy polimorfizmu i abstrakcji

JAVA 25 SELF
Poziom 23, Lekcja 3
Dostępny

1. Polimorfizm: co to jest i po co jest potrzebny

Jeśli myślisz, że polimorfizm to coś ze świata mutantów Marvela, muszę rozczarować: w programowaniu wszystko jest dużo spokojniejsze, ale nie mniej magiczne. Polimorfizm to zdolność obiektów o różnej implementacji reagować na te same wywołania metod w różny sposób.

Przykład z życia:
Masz klasę Book i klasę Magazine, obie dziedziczą po klasie abstrakcyjnej LibraryItem. Chcesz, aby dla dowolnego elementu biblioteki można było wywołać metodę printInfo(), a ona wypisze potrzebne informacje — dla książki będzie to autor i tytuł, dla magazynu — numer wydania i data.

Przykład kodu:

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("Książka: " + title + ", autor: " + author);
    }
}

class Magazine extends LibraryItem {
    int issueNumber;

    Magazine(String title, int issueNumber) {
        super(title);
        this.issueNumber = issueNumber;
    }

    @Override
    void printInfo() {
        System.out.println("Magazyn: " + title + ", wydanie: " + issueNumber);
    }
}

Teraz można utworzyć tablicę z różnych elementów i wywołać printInfo() dla każdego:

LibraryItem[] items = {
    new Book("Władca much", "William Golding"),
    new Magazine("Nauka i życie", 5)
};

for (LibraryItem item : items) {
    item.printInfo();
}
// Wyświetli:
// Książka: Władca much, autor: William Golding
// Magazyn: Nauka i życie, wydanie: 5

Tak właśnie działa polimorfizm!

2. Typowe błędy z polimorfizmem

Próba wywołania metod, których nie ma w typie bazowym

Jednym z najczęstszych błędów jest próba odwołania się do metody, która jest zadeklarowana tylko w klasie potomnej, przez referencję typu bazowego.

LibraryItem item = new Book("Harry Potter", "J.K. Rowling");
// item.getAuthor(); // Błąd kompilacji! W LibraryItem nie ma metody getAuthor()

Java kompiluje kod, wychodząc od tego, co widzi w typie zmiennej (LibraryItem), a nie w rzeczywistym obiekcie (Book). Dlatego jeśli potrzebujesz wywołać metodę specyficzną dla książki, musisz dokonać rzutowania:

if (item instanceof Book) {
    Book book = (Book) item;
    // Teraz można wywołać book.getAuthor()
}

Rzutowanie bez sprawdzenia

Jeśli jesteś przekonany, że obiekt to Book, ale w rzeczywistości tak nie jest, dostaniesz ClassCastException w czasie działania programu. Na przykład:

LibraryItem item = new Magazine("Forbes", 12);
Book book = (Book) item; // Bum! ClassCastException

Prawidłowy sposób — zawsze sprawdzać typ:

if (item instanceof Book) {
    Book book = (Book) item;
    // OK
} else {
    System.out.println("To nie jest książka!");
}

Nieużywanie zalet polimorfizmu

Czasem programiści piszą kod tak, że jest on sztywno związany z konkretnymi typami, chociaż można by użyć abstrakcji. Na przykład, jeśli piszesz:

Book[] books = ...;
for (Book book : books) {
    book.printInfo();
}

To działa tylko dla książek. A jeśli jutro pojawią się magazyny, gazety, komiksy? Lepiej używać tablicy LibraryItem[] i pracować z metodami klasy bazowej lub interfejsu.

3. Abstrakcje: po co są i jak ich nie zepsuć

Klasy abstrakcyjne i interfejsy

Abstrakcja to sztuka wyodrębnienia tego, co najważniejsze, i ukrycia szczegółów. W Javie służą do tego klasy abstrakcyjne i interfejsy.

  • Klasa abstrakcyjna — klasa, której nie można utworzyć bezpośrednio, a jedynie po niej dziedziczyć.
  • Interfejs — kontrakt: co klasa ma umieć, ale nie jak to robi.

Błąd 1: Utworzenie klasy abstrakcyjnej bez metod abstrakcyjnych

Jeśli w twojej klasie abstrakcyjnej nie ma żadnej metody abstrakcyjnej, zastanów się — czy na pewno powinna być abstrakcyjna? Może prościej zrobić zwykłą klasę?

abstract class UselessAbstract {
    void sayHello() {
        System.out.println("Hello!");
    }
}
// Lepiej zrobić zwykłą klasę, jeśli nie ma metod abstrakcyjnych

Błąd 2: Brak implementacji obowiązkowych metod w klasach potomnych

Jeśli klasa dziedziczy po klasie abstrakcyjnej lub implementuje interfejs, ma obowiązek zaimplementować wszystkie metody abstrakcyjne. Jeśli o tym zapomnisz — kompilator przypomni, ale bywa, że metody są zaimplementowane „na odczepnego” i nic nie robią. To utrudnia utrzymanie kodu.

class Magazine extends LibraryItem {
    Magazine(String title, int issueNumber) {
        super(title);
        // ...
    }

    @Override
    void printInfo() {
        // Pusto! Źle!
    }
}

Błąd 3: Zbyt głęboka lub zagmatwana hierarchia abstrakcji

Gdy klasy dziedziczą po sobie w pięciu–dziesięciu poziomach, zrozumienie ich staje się bardzo trudne. Lepiej tworzyć „płaskie” hierarchie, w których wszystko jest jasne.

Zły przykład:

LibraryItem
  |
  BookItem
    |
    PrintedBook
      |
      IllustratedBook
        |
        ChildrenIllustratedBook

Skomplikowane, prawda? Lepiej ograniczyć się do dwóch–trzech poziomów.

4. Praktyka: Zastosowanie polimorfizmu i abstrakcji w aplikacji ćwiczeniowej

Dopracujmy twoją ćwiczeniową aplikację biblioteczną. Wcześniej były tylko książki. Teraz dodamy magazyny i zrealizujemy wspólny interfejs dla wydań drukowanych.

Ogłośmy klasę abstrakcyjną:

abstract class LibraryItem {
    protected String title;

    public LibraryItem(String title) {
        this.title = title;
    }

    public abstract void printInfo();
}

Dodajmy klasy potomne:

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("Książka: " + title + ", autor: " + 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("Magazyn: " + title + ", wydanie: " + issueNumber);
    }
}

Używamy polimorfizmu:

LibraryItem[] items = {
    new Book("Czysty kod", "Robert Martin"),
    new Magazine("Java World", 3)
};

for (LibraryItem item : items) {
    item.printInfo();
}

Dodajmy interfejs dla wydań elektronicznych

Załóżmy, że niektóre wydania można czytać online. Wprowadźmy interfejs:

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("Otwieramy e-książkę pod adresem: " + url);
    }
}

Teraz można pracować z e-książkami przez interfejs:

ReadableOnline ebook = new EBook("Java dla początkujących", "Barry Burd", "https://example.com/java");
ebook.openOnline();

5. Jak uniknąć problemów z polimorfizmem i abstrakcjami: najlepsze praktyki

  • Używaj interfejsów i klas abstrakcyjnych do opisu zachowań, a nie stanu.
    Na przykład interfejs Printable dobrze opisuje możliwość „drukowania”, ale trzymanie w interfejsie pola String title to już zły pomysł.
  • Sprawdzaj typ obiektu za pomocą instanceof przed rzutowaniem.
    Zwłaszcza jeśli obiekt może mieć różny typ. To uchroni przed ClassCastException.
  • Dąż do „płaskich” i zrozumiałych hierarchii.
    Im prostsze drzewo dziedziczenia — tym łatwiej utrzymywać i rozbudowywać kod.
  • Unikaj „bezsensownych” abstrakcji.
    Jeśli klasa nie zawiera metod abstrakcyjnych i nie jest przeznaczona do dziedziczenia — nie rób jej abstrakcyjną.
  • Używaj adnotacji @Override zawsze, gdy nadpisujesz metodę.
    To pomaga kompilatorowi wyłapywać błędy w sygnaturze.

6. Typowe błędy przy pracy z polimorfizmem i abstrakcjami

Błąd nr 1: Rzutowanie typu bez sprawdzenia

Czasem kusi, by „pójść na skróty” i rzutować typ bez sprawdzenia. Może to zadziałać, ale może też doprowadzić do nagłego upadku programu. Zawsze używaj instanceof:

if (item instanceof Book) {
    Book book = (Book) item;
    // ...
}

Błąd nr 2: Próba wywołania metody klasy potomnej przez referencję typu bazowego

LibraryItem item = new Book("Java", "Autor");
item.getAuthor(); // Błąd kompilacji: w LibraryItem nie ma takiej metody!

Rozwiązanie — albo rzutować typ, albo dodać potrzebną metodę do klasy bazowej (jeśli to ma sens).

Błąd nr 3: Niepełna implementacja interfejsu lub klasy abstrakcyjnej

Jeśli zapomnisz zaimplementować wszystkie metody interfejsu — kompilator nie pozwoli zbudować projektu. Ale jeśli zaimplementujesz „wydmuszki”, które nic nie robią, doprowadzi to do nieoczekiwanego zachowania.

Błąd nr 4: Zbyt głęboka hierarchia dziedziczenia

Jeśli masz więcej niż trzy poziomy dziedziczenia — zastanów się, czy nie da się uprościć architektury.

Błąd nr 5: Naruszenie zasady pojedynczej odpowiedzialności

Jeśli abstrakcja opisuje zbyt wiele obowiązków, staje się trudna w utrzymaniu. Lepiej rozbijać na kilka interfejsów lub klas.

1
Zadanie
JAVA 25 SELF, poziom 23, lekcja 3
Niedostępne
Jedna metoda dla każdego pliku 🎬
Jedna metoda dla każdego pliku 🎬
1
Zadanie
JAVA 25 SELF, poziom 23, lekcja 3
Niedostępne
Wirtualne płótno: Różnorodność kształtów 🎨
Wirtualne płótno: Różnorodność kształtów 🎨
1
Zadanie
JAVA 25 SELF, poziom 23, lekcja 3
Niedostępne
Inteligentny dom: Wrzący czajnik 💡
Inteligentny dom: Wrzący czajnik 💡
1
Zadanie
JAVA 25 SELF, poziom 23, lekcja 3
Niedostępne
Firma IT: Programista, który pracuje i raportuje 👨‍💻
Firma IT: Programista, który pracuje i raportuje 👨‍💻
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION