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.
GO TO FULL VERSION