1. Was ist eine Klassenhierarchie?
In der Programmierung (und nicht nur dort) ist eine Hierarchie eine baumartige Struktur: Oben im Baum befinden sich „allgemeine“ Entitäten (Basisklassen), darunter – spezifischere (Unterklassen). In Java wird eine Klassenhierarchie mittels Vererbung aufgebaut: Jede Unterklasse kann ihrerseits Elternteil für weitere Unterklassen sein, und so weiter.
Analogie:
- Ein Tier (Animal) ist eine allgemeine Klasse.
- Ein Säugetier (Mammal) ist ein spezieller Fall eines Tieres.
- Ein Hund (Dog) ist ein spezieller Fall eines Säugetiers.
- Und ein konkreter „Buddy“ ist bereits ein Objekt der Klasse Dog.
Im Code sieht das so aus:
class Animal { }
class Mammal extends Animal { }
class Dog extends Mammal { }
Eine Klassenhierarchie erlaubt es, gemeinsame Eigenschaften und Verhalten „oben“ zu beschreiben und Details „unten“. Das macht den Code logischer und vermeidet Duplizierung.
Hierarchieschema
Animal
├── Mammal
│ ├── Dog
│ └── Cat
└── Bird
└── Sparrow
2. Wie man eine Hierarchie aufbaut: Logik und Praxis
Allgemeines und Spezielles bestimmen
Die Grundregel: Die Basisklasse enthält das, was für alle ihre Nachkommen charakteristisch ist. Alles Einzigartige gehört in die Unterklassen.
Beispiel:
- Alle Tiere können atmen und fressen – also sollten die Methoden breathe() und eat() in der Klasse Animal sein.
- Nur Vögel können fliegen – daher gehört die Methode fly() in die Klasse Bird, nicht in Animal.
- Nur Hunde können bellen – die Methode bark() gehört in die Klasse Dog.
Beispiel: Tiere
// Basisklasse
class Animal {
String name;
Animal(String name) {
this.name = name;
}
void eat() {
System.out.println(name + " isst.");
}
void makeSound() {
System.out.println(name + " gibt ein Geräusch von sich.");
}
}
// Unterklasse: Säugetier
class Mammal extends Animal {
Mammal(String name) {
super(name);
}
void feedMilk() {
System.out.println(name + " füttert die Jungen mit Milch.");
}
}
// Unterklasse: Hund
class Dog extends Mammal {
Dog(String name) {
super(name);
}
@Override
void makeSound() {
System.out.println(name + " bellt: Wau-wau!");
}
void wagTail() {
System.out.println(name + " wedelt mit dem Schwanz.");
}
}
// Unterklasse: Katze
class Cat extends Mammal {
Cat(String name) {
super(name);
}
@Override
void makeSound() {
System.out.println(name + " miaut: Miau!");
}
void purr() {
System.out.println(name + " schnurrt.");
}
}
// Unterklasse: Vogel
class Bird extends Animal {
Bird(String name) {
super(name);
}
void fly() {
System.out.println(name + " fliegt.");
}
@Override
void makeSound() {
System.out.println(name + " zwitschert: Tschilp-tschilp!");
}
}
Aufruf in main:
public class ZooDemo {
public static void main(String[] args) {
Dog buddy = new Dog("Buddy");
Cat fluffy = new Cat("Fluffy");
Bird sparrow = new Bird("Spatz");
buddy.eat(); // Buddy isst.
buddy.makeSound(); // Buddy bellt: Wau-wau!
buddy.feedMilk(); // Buddy füttert die Jungen mit Milch.
buddy.wagTail(); // Buddy wedelt mit dem Schwanz.
fluffy.eat(); // Fluffy isst.
fluffy.makeSound(); // Fluffy miaut: Miau!
fluffy.feedMilk(); // Fluffy füttert die Jungen mit Milch.
fluffy.purr(); // Fluffy schnurrt.
sparrow.eat(); // Spatz isst.
sparrow.makeSound(); // Spatz zwitschert: Tschilp-tschilp!
sparrow.fly(); // Spatz fliegt.
}
}
Visualisierung: Klassenbaum
| Klasse | Elternklasse | Besonderheiten |
|---|---|---|
|
|
name, eat(), makeSound() |
|
|
feedMilk() |
|
|
makeSound(), wagTail() |
|
|
makeSound(), purr() |
|
|
fly(), makeSound() |
3. Weitere Beispiele aus dem Alltag
Geometrische Figuren
Klassenhierarchien eignen sich hervorragend zur Modellierung von Geometrie.
// Basisklasse
class Shape {
void draw() {
System.out.println("Wir zeichnen eine Figur.");
}
}
// Kreis
class Circle extends Shape {
double radius;
Circle(double radius) {
this.radius = radius;
}
@Override
void draw() {
System.out.println("Wir zeichnen einen Kreis mit Radius " + radius);
}
}
// Rechteck
class Rectangle extends Shape {
double width, height;
Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
void draw() {
System.out.println("Wir zeichnen ein Rechteck " + width + "x" + height);
}
}
Verwendung:
public class ShapeDemo {
public static void main(String[] args) {
Shape s1 = new Circle(5);
Shape s2 = new Rectangle(3, 4);
s1.draw(); // Wir zeichnen einen Kreis mit Radius 5.0
s2.draw(); // Wir zeichnen ein Rechteck 3.0x4.0
}
}
Transport
class Vehicle {
void move() {
System.out.println("Das Fahrzeug bewegt sich.");
}
}
class Car extends Vehicle {
@Override
void move() {
System.out.println("Das Auto fährt auf der Straße.");
}
}
class Bicycle extends Vehicle {
@Override
void move() {
System.out.println("Der Radfahrer tritt in die Pedale.");
}
}
main:
Vehicle v1 = new Car();
Vehicle v2 = new Bicycle();
v1.move(); // Das Auto fährt auf der Straße.
v2.move(); // Der Radfahrer tritt in die Pedale.
Benutzer
class User {
String username;
User(String username) { this.username = username; }
void login() { System.out.println(username + " hat sich angemeldet."); }
}
class Admin extends User {
Admin(String username) { super(username); }
void deleteUser(String user) {
System.out.println(username + " hat den Benutzer " + user + " gelöscht");
}
}
class Customer extends User {
Customer(String username) { super(username); }
void buy() { System.out.println(username + " hat einen Kauf getätigt."); }
}
4. Nützliche Feinheiten
Übertreiben Sie es nicht mit Vererbung. Vererbung ist ein Werkzeug für „is-a“-Beziehungen. Wenn Sie sagen wollen: „Die Katze ist ein Tier“, verwenden Sie Vererbung. Wenn hingegen „Die Katze hat einen Schwanz“, verwenden Sie Komposition (has-a).
Schlecht:
class Engine { /* ... */ }
class Car extends Engine { /* Ist ein Auto ein Motor? Nein, das ist Komposition! */ }
Gut:
class Car {
Engine engine; // Das Auto enthält einen Motor
}
Vermeiden Sie zu tiefe Hierarchien. Je mehr Ebenen – desto schwieriger sind Wartung und Verständnis des Codes. In der Regel sind 2–3 Ebenen für die meisten Aufgaben das Maximum.
Vermeiden Sie auch zu „flache“ Hierarchien. Wenn Sie 20 Klassen haben, die alle direkt von einer Basisklasse erben, sollten Sie die Architektur überdenken.
Vererbung „der Bequemlichkeit halber“. Manchmal möchte man Methoden oder Felder „stehlen“, ohne über die Logik nachzudenken. Das führt ins Chaos! Wenn class A und class B logisch nicht zusammenhängen, sollte man class B extends A nicht nur wegen ein paar Methoden machen.
Verstoß gegen das Liskovsche Substitutionsprinzip. Wenn eine Unterklasse nicht ohne Überraschungen anstelle der Oberklasse eingesetzt werden kann, ist die Hierarchie falsch aufgebaut.
Code-Duplikation. Wenn Sie sich dabei ertappen: „Oh, diese Methode kopiere ich von Klasse zu Klasse“, ist es wohl Zeit, sie in die Basisklasse auszulagern.
5. Typische Fehler beim Aufbau von Klassenhierarchien
Fehler Nr. 1: Vererbung ohne „is-a“-Logik.
Wenn Sie Vererbung nur einsetzen, um Zugriff auf Methoden oder Felder zu bekommen, und nicht, weil die Unterklasse wirklich „ist“ wie die Oberklasse, wird Ihr Code schnell zum Wirrwarr. Zum Beispiel: „Das Auto erbt den Motor“ – das ist kein is-a, sondern has-a.
Fehler Nr. 2: Einzigartige Besonderheiten der Unterklasse ignorieren.
Wenn all Ihre Unterklassen gleich aussehen und nichts Neues hinzufügen, ist die Hierarchie möglicherweise überflüssig – verwenden Sie eine einzige Klasse.
Fehler Nr. 3: Code-Duplikation in Unterklassen.
Wenn Sie dieselbe Implementierung in mehrere Unterklassen kopieren, sollten Sie sie in die Oberklasse verschieben.
Fehler Nr. 4: Zu komplexe oder zu tiefe Hierarchie.
Mehrstufige Hierarchien sind schwer zu warten und zu testen. Je einfacher – desto besser!
Fehler Nr. 5: Methodenüberschreibung ohne die Annotation @Override.
Ohne die Annotation ist es leicht, sich in der Methodensignatur zu vertun; dann wird die Methode nicht überschrieben, sondern als neu angesehen. Verwenden Sie @Override immer!
GO TO FULL VERSION