CodeGym /Kurse /JAVA 25 SELF /Probleme mit Polymorphismus und Abstraktionen

Probleme mit Polymorphismus und Abstraktionen

JAVA 25 SELF
Level 23, Lektion 3
Verfügbar

1. Polymorphismus: was ist das und wozu braucht man ihn

Wenn Sie denken, Polymorphismus sei etwas aus der Welt der Marvel-Mutanten, muss ich Sie enttäuschen: In der Programmierung geht es viel ruhiger zu – aber nicht weniger magisch. Polymorphismus ist die Fähigkeit von Objekten mit unterschiedlicher Implementierung, auf dieselben Methodenaufrufe unterschiedlich zu reagieren.

Beispiel aus dem Alltag:
Sie haben die Klasse Book und die Klasse Magazine, beide erben von der abstrakten Klasse LibraryItem. Sie möchten, dass man für jedes Bibliothekselement die Methode printInfo() aufrufen kann und sie die gewünschten Informationen ausgibt – für ein Buch sind das Autor und Titel, für eine Zeitschrift – Ausgabennummer und Datum.

Codebeispiel:

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("Buch: " + 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("Zeitschrift: " + title + ", Ausgabe: " + issueNumber);
    }
}

Jetzt kann man ein Array aus verschiedenen Elementen erstellen und für jedes printInfo() aufrufen:

LibraryItem[] items = {
    new Book("Herr der Fliegen", "William Golding"),
    new Magazine("Wissenschaft und Leben", 5)
};

for (LibraryItem item : items) {
    item.printInfo();
}
// Ausgabe:
// Buch: Herr der Fliegen, Autor: William Golding
// Zeitschrift: Wissenschaft und Leben, Ausgabe: 5

So funktioniert Polymorphismus!

2. Typische Fehler mit Polymorphismus

Versuch, Methoden aufzurufen, die im Basistyp nicht existieren

Einer der häufigsten Fehler ist der Versuch, über eine Referenz auf den Basistyp eine Methode aufzurufen, die nur in der abgeleiteten Klasse deklariert ist.

LibraryItem item = new Book("Harry Potter", "J. Rowling");
// item.getAuthor(); // Kompilierfehler! In LibraryItem gibt es keine Methode getAuthor()

Java kompiliert Code auf Basis des deklarierten Typs der Variablen (LibraryItem) und nicht des tatsächlichen Objekts (Book). Wenn Sie also eine buch-spezifische Methode aufrufen müssen, sollten Sie den Typ umwandeln:

if (item instanceof Book) {
    Book book = (Book) item;
    // Jetzt kann book.getAuthor() aufgerufen werden
}

Type-Casts ohne Prüfung

Wenn Sie sicher sind, dass das Objekt eine Book ist, es aber in Wirklichkeit nicht ist, erhalten Sie zur Laufzeit eine ClassCastException. Zum Beispiel:

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

Richtig ist es – den Typ immer zu prüfen:

if (item instanceof Book) {
    Book book = (Book) item;
    // OK
} else {
    System.out.println("Das ist kein Buch!");
}

Polymorphievorteile nicht nutzen

Manchmal schreiben Entwickler Code so, dass er hart an konkrete Typen gebunden ist, obwohl man Abstraktionen nutzen könnte. Wenn Sie zum Beispiel schreiben:

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

Das funktioniert nur für Bücher. Und wenn morgen Zeitschriften, Zeitungen, Comics dazukommen? Besser ist es, ein Array LibraryItem[] zu verwenden und mit den Methoden der Basisklasse oder des Interfaces zu arbeiten.

3. Abstraktionen: wozu sie gut sind und wie man sie richtig einsetzt

Abstrakte Klassen und Interfaces

Abstraktion ist die Kunst, das Wesentliche herauszuarbeiten und Details zu verbergen. In Java gibt es dafür abstrakte Klassen und Interfaces.

  • Abstrakte Klasse – eine Klasse, die nicht direkt instanziiert, sondern nur vererbt werden kann.
  • Interface – ein Vertrag: was eine Klasse können muss, aber nicht wie sie es tut.

Fehler 1: Eine abstrakte Klasse ohne abstrakte Methoden erstellen

Wenn Ihre abstrakte Klasse keine einzige abstrakte Methode hat, überlegen Sie – muss sie wirklich abstrakt sein? Vielleicht ist eine normale Klasse einfacher?

abstract class UselessAbstract {
    void sayHello() {
        System.out.println("Hello!");
    }
}
// Besser eine normale Klasse verwenden, wenn es keine abstrakten Methoden gibt

Fehler 2: Fehlende Implementierung obligatorischer Methoden in Unterklassen

Wenn eine Klasse von einer abstrakten Klasse erbt oder ein Interface implementiert, muss sie alle abstrakten Methoden implementieren. Wenn man es vergisst – erinnert der Compiler daran, aber es kommt vor, dass Methoden „pro forma“ implementiert werden und nichts tun. Das ist schlecht für die Wartbarkeit.

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

    @Override
    void printInfo() {
        // Leer! Schlecht!
    }
}

Fehler 3: Zu tiefe oder verworrene Abstraktionshierarchie

Wenn Klassen über fünf–zehn Ebenen voneinander erben, wird es sehr schwer, den Überblick zu behalten. Besser sind „flache“ Hierarchien, bei denen alles verständlich ist.

Schlechtes Beispiel:

LibraryItem
  |
  BookItem
    |
    PrintedBook
      |
      IllustratedBook
        |
        ChildrenIllustratedBook

Komplex, nicht wahr? Beschränken Sie sich besser auf zwei bis drei Ebenen.

4. Praxis: Einsatz von Polymorphismus und Abstraktionen in einer Beispielanwendung

Verbessern wir Ihre Lernanwendung für die Bibliothek. Bisher hatten Sie nur Bücher. Jetzt fügen wir Zeitschriften hinzu und implementieren eine gemeinsame Schnittstelle für Printmedien.

Deklarieren wir eine abstrakte Klasse:

abstract class LibraryItem {
    protected String title;

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

    public abstract void printInfo();
}

Fügen wir abgeleitete Klassen hinzu:

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("Buch: " + 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("Zeitschrift: " + title + ", Ausgabe: " + issueNumber);
    }
}

Polymorphismus nutzen:

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

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

Fügen wir ein Interface für elektronische Ausgaben hinzu

Angenommen, einige Ausgaben kann man online lesen. Führen wir ein Interface ein:

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("Wir öffnen das E-Book unter: " + url);
    }
}

Jetzt kann man mit E-Books über das Interface arbeiten:

ReadableOnline ebook = new EBook("Java für Dummies", "Barry Burd", "https://example.com/java");
ebook.openOnline();

5. Wie man Probleme mit Polymorphismus und Abstraktionen vermeidet: Best Practices

  • Verwenden Sie Interfaces und abstrakte Klassen, um Verhalten zu beschreiben, nicht Zustand.
    Zum Beispiel beschreibt das Interface Printable die Fähigkeit „zu drucken“ gut, aber ein Feld String title in einem Interface zu halten, ist schon eine schlechte Idee.
  • Prüfen Sie den Typ mit instanceof vor dem Cast.
    Insbesondere, wenn das Objekt unterschiedliche Typen haben kann. Das schützt vor ClassCastException.
  • Streben Sie „flache“ und verständliche Hierarchien an.
    Je einfacher der Vererbungsbaum – desto leichter sind Code-Wartung und -Erweiterung.
  • Vermeiden Sie „sinnlose“ Abstraktionen.
    Wenn die Klasse keine abstrakten Methoden enthält und nicht zur Vererbung gedacht ist – machen Sie sie nicht abstrakt.
  • Verwenden Sie die Annotation @Override immer, wenn Sie eine Methode überschreiben.
    Das hilft dem Compiler, Signaturfehler zu finden.

6. Typische Fehler bei der Arbeit mit Polymorphismus und Abstraktionen

Fehler Nr. 1: Cast ohne Prüfung

Manchmal möchte man „Abkürzungen“ nehmen und ohne Prüfung casten. Das kann funktionieren – oder zu einem plötzlichen Absturz führen. Verwenden Sie immer instanceof:

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

Fehler Nr. 2: Versuch, eine Methode der Unterklasse über eine Referenz auf den Basistyp aufzurufen

LibraryItem item = new Book("Java", "Autor");
item.getAuthor(); // Kompilierfehler: In LibraryItem gibt es keine solche Methode!

Lösung – entweder den Typ casten oder die benötigte Methode in die Basisklasse aufnehmen (wenn das logisch ist).

Fehler Nr. 3: Unvollständige Implementierung eines Interfaces oder einer abstrakten Klasse

Wenn man vergisst, alle Methoden eines Interfaces zu implementieren, lässt der Compiler das Projekt nicht bauen. Wenn man jedoch „Platzhalter“ implementiert, die nichts tun, führt das zu unerwartetem Verhalten.

Fehler Nr. 4: Zu tiefe Vererbungshierarchie

Wenn Sie mehr als drei Vererbungsebenen haben – überlegen Sie, ob sich die Architektur nicht vereinfachen lässt.

Fehler Nr. 5: Verletzung des Single-Responsibility-Prinzips

Wenn eine Abstraktion zu viele Verantwortlichkeiten beschreibt, wird sie schwer wartbar. Besser auf mehrere Interfaces oder Klassen aufteilen.

1
Aufgabe
JAVA 25 SELF, Level 23, Lektion 3
Gesperrt
Eine Methode für jede Datei 🎬
Eine Methode für jede Datei 🎬
1
Aufgabe
JAVA 25 SELF, Level 23, Lektion 3
Gesperrt
Virtuelle Leinwand: Vielfalt an Formen 🎨
Virtuelle Leinwand: Vielfalt an Formen 🎨
1
Aufgabe
JAVA 25 SELF, Level 23, Lektion 3
Gesperrt
Smart Home: Kochender Wasserkocher 💡
Smart Home: Kochender Wasserkocher 💡
1
Aufgabe
JAVA 25 SELF, Level 23, Lektion 3
Gesperrt
IT-Unternehmen: Der Programmierer, der arbeitet und Bericht erstattet 👨‍💻
IT-Unternehmen: Der Programmierer, der arbeitet und Bericht erstattet 👨‍💻
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION