1. Einschränkungen der Vererbung in Java
Nur einfache Klassenvererbung. In Java kann eine Klasse nur von genau einer anderen Klasse erben. Das nennt man einfache Vererbung. Zum Beispiel – das ist erlaubt:
class Animal { }
class Dog extends Animal { }
Aber so – geht es nicht:
class Animal { }
class Robot { }
// FEHLER! Java unterstützt keine Mehrfachvererbung von Klassen
class RoboDog extends Animal, Robot { }
Wenn man versucht, eine solche Klasse zu deklarieren, meldet der Compiler: "class RoboDog cannot extend multiple classes". Warum ist das so? Weil Mehrfachvererbung zu Mehrdeutigkeiten führt: Wenn beide Eltern eine Methode mit derselben Signatur haben, welche soll verwendet werden? Das ist das bekannte „Diamantproblem“ (diamond problem).
Schnittstellen kann man in Java beliebig viele implementieren, aber die haben wir noch nicht behandelt. Wir sprechen später darüber.
Konstruktoren werden nicht vererbt. Selbst wenn die Elternklasse einen praktischen Konstruktor hat, erscheint dieser nicht automatisch in der Kindklasse. Man muss den Eltern-Konstruktor explizit über super(...) im Konstruktor der Kindklasse aufrufen.
Private Mitglieder werden nicht vererbt. Alle privaten (private) Felder und Methoden der Elternklasse sind in der Kindklasse nicht verfügbar. Sie existieren „im Inneren“ des Objekts, aber direkt darauf zugreifen kann man nicht.
2. Probleme einer fragilen Hierarchie
Starke Kopplung zwischen Klassen. Wenn Sie eine Klassenhierarchie erstellen, sind Unterklassen eng mit der Basisklasse gekoppelt. Ändern Sie die Basisklasse, kann das alle ihre Unterklassen betreffen (oder sogar brechen). Stellen Sie sich vor, Sie haben eine Klasse Animal, von der Dog, Cat, Bird und noch ein Dutzend andere erben. Wenn Sie die Struktur von Animal ändern (zum Beispiel einen neuen Pflichtparameter im Konstruktor hinzufügen), müssen Sie alle Kindklassen durchgehen und ihren Code aktualisieren. Das ist besonders problematisch in großen Projekten.
Problem der „brechenden“ Vererbung. Manchmal kann eine Unterklasse unbeabsichtigt ein Verhalten verändern, auf das sich die Basisklasse verlässt. Zum Beispiel ruft die Basisklasse ihre eigene Methode innerhalb einer anderen Methode auf, und die Unterklasse überschreibt diese Methode und ändert ihre Logik. Infolgedessen verhält sich die Basisklasse nicht mehr wie erwartet.
class Animal {
void makeSound() {
System.out.println("Some sound");
}
void sleep() {
System.out.println("Animal is going to sleep...");
makeSound(); // Die Elternklasse ruft ihre eigene Methode auf
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Woof!");
}
}
public class Main {
public static void main(String[] args) {
Animal a = new Dog();
a.sleep();
}
}
Was gibt das Programm aus?
Animal is going to sleep...
Woof!
Die Basisklasse ging davon aus, dass makeSound() ihre eigene Implementierung ist, tatsächlich wird jedoch die Variante aus der Unterklasse aufgerufen! Das kann zu unerwarteten Bugs führen, wenn die Unterklasse die Methode mit anderer Logik überschreibt.
3. Das Problem der fragilen Basisklasse (fragile base class problem)
Das ist ein echtes Problem in großen Projekten. Wenn Sie die Basisklasse ändern (zum Beispiel ein Feld hinzufügen oder die Implementierung einer Methode ändern), riskieren Sie, das Verhalten aller Unterklassen zu beschädigen. Manchmal zeigt sich das nicht sofort, und die Fehlersuche kann Stunden oder sogar Tage dauern.
Illustration: Angenommen, Sie haben eine Klasse Shape mit der Methode draw(). Sie entscheiden, in Shape eine neue Methode drawShadow() hinzuzufügen, die draw() aufruft. Aber eine der Unterklassen (Circle) überschreibt draw(), und nun kann sich das Verhalten beim Aufruf von drawShadow() auf Circle als unerwartet erweisen.
4. Starke Kopplung und Schwierigkeiten beim Refactoring
Wenn Klassen durch Vererbung gekoppelt sind, kann die Änderung einer Klasse eine ganze Kette von Abhängigkeiten betreffen. Das macht den Code weniger flexibel, erschwert Refactoring und Erweiterungen. Manchmal muss man ganze Hierarchien umschreiben, um neue Funktionalität hinzuzufügen.
Praxisbeispiel
class Vehicle { /* ... */ }
class Car extends Vehicle { /* ... */ }
class Bicycle extends Vehicle { /* ... */ }
class Bus extends Vehicle { /* ... */ }
Plötzlich kommt die Anforderung: „Fügen wir doch einen E-Scooter hinzu!“. Aber ein E-Scooter ist sowohl ein Transportmittel als auch ein Gadget. Wie geht man vor? Wenn Sie beginnen, die Hierarchie zu erweitern, um alle neuen Entitäten hineinzupressen, wird sie schnell unbeherrschbar.
5. Problem der Codewiederverwendung ohne fachliche Beziehung
Sehr häufig nutzen Einsteiger (und nicht nur sie) Vererbung zur Wiederverwendung von Code, selbst wenn zwischen den Klassen keine „ist-ein“-Beziehung (is-a) besteht. Das führt zu einer falschen Architektur.
Beispiel für fehlerhafte Vererbung
class DatabaseUtils {
void connect() { /* ... */ }
void disconnect() { /* ... */ }
}
class User extends DatabaseUtils { // Ein Benutzer ist keine „Datenbank-Utility“!
String name;
}
Besser ist es, Komposition zu verwenden: DatabaseUtils als eigene Klasse belassen und deren Methoden an den benötigten Stellen aufrufen, statt davon zu erben.
6. Alternativen zur Vererbung
Komposition (has-a)
Wenn ein Objekt ein anderes „enthält“, verwenden Sie Komposition. Zum Beispiel kann die Klasse Car ein Feld Engine haben:
class Engine { /* ... */ }
class Car {
private Engine engine;
// ...
}
Delegation
Anstatt eine Klasse zu erweitern, delegieren Sie die Ausführung der Aufgabe an ein anderes Objekt. Das erhält die Flexibilität und verringert die Kopplung der Komponenten.
Schnittstellen
In Java kann eine Klasse beliebig viele Schnittstellen implementieren. So lässt sich Verhalten flexibel kombinieren, ohne eine starre Hierarchie. Zu Schnittstellen kommen wir später zurück.
Wann sollte man Vererbung einsetzen?
Verwenden Sie Vererbung nur, wenn zwischen den Klassen eine klare „ist-ein“-Beziehung (is-a) besteht:
- Eine Katze ist ein Tier (Cat extends Animal)
- Ein Kreis ist eine Figur (Circle extends Shape)
- Ein Administrator ist ein Benutzer (Admin extends User)
Verwenden Sie Vererbung nicht nur zur Wiederverwendung von Code – dafür gibt es Komposition und Delegation.
7. Einige praktische Beispiele
Beispiel: überkomplizierte Hierarchie
class Animal { }
class Mammal extends Animal { }
class Cat extends Mammal { }
class PersianCat extends Cat { }
class SuperPersianCat extends PersianCat { }
Wenn Ihre Hierarchie tiefer als drei Ebenen geht – überlegen Sie: Ist es nicht Zeit, innezuhalten? Zu tiefe Hierarchien erschweren das Verständnis und die Wartung des Codes.
Beispiel: flache Hierarchie
class Animal { }
class Cat extends Animal { }
class Dog extends Animal { }
class Bird extends Animal { }
class Fish extends Animal { }
class Spider extends Animal { }
class Platypus extends Animal { }
class Dragon extends Animal { }
Wenn Sie Dutzende von Unterklassen haben, die sich jeweils nur in einer Methode unterscheiden, sollten Sie vielleicht Schnittstellen oder Komposition verwenden.
8. Typische Fehler beim Einsatz von Vererbung
Fehler Nr. 1: Vererbung ohne „is-a“-Beziehung.
Wenn die Unterklasse in Wirklichkeit keine Variante der Basisklasse ist, wird die Architektur unnatürlich und gerät schnell außer Kontrolle. Zum Beispiel sollte die Klasse User nicht von DatabaseUtils erben, selbst wenn das „praktisch“ erscheint.
Fehler Nr. 2: Methoden überschreiben und dabei den Vertrag ändern.
Wenn Sie eine Methode überschreiben und ihre Logik so verändern, dass sie nicht mehr den Erwartungen der Basisklasse entspricht, führt das zu unerwarteten Fehlern. Wenn die Basisklasse zum Beispiel erwartet, dass draw() eine Figur zeichnet, die Methode in der Unterklasse aber plötzlich gefährliche Seiteneffekte ausführt – ist das katastrophal.
Fehler Nr. 3: Zu tiefe oder zu flache Hierarchien.
Eine zu tiefe Hierarchie erschwert das Verständnis des Codes; eine zu flache führt zu Duplizierung.
Fehler Nr. 4: Der Versuch, Sprachbeschränkungen zu umgehen.
Man versucht, Mehrfachvererbung mit „Krücken“ zu simulieren (Copy-Paste, „Utility“-Superklassen), was im Chaos endet.
Fehler Nr. 5: Blindes Verwenden von Vererbung zur Codewiederverwendung.
Das führt oft zu unerwarteten Verbindungen zwischen Klassen, erschwert das Testen und die Wartung. Verwenden Sie Komposition und Delegation.
GO TO FULL VERSION