1. Methodenüberschreibung
Überschreiben (overriding) – die Möglichkeit, in einer Unterklasse eine eigene Version einer Methode zu implementieren, die bereits in der Oberklasse existiert. Dadurch funktioniert echter Polymorphismus zur Laufzeit (run-time).
Einfach gesagt:
Wenn Sie eine Basisklasse mit einer Methode haben und möchten, dass die Unterklasse diese Methode auf ihre eigene Weise ausführt, deklarieren Sie in der Unterklasse einfach eine Methode mit derselben Signatur. Beim Aufruf der Methode über eine Referenz des Basistyps wird die Version der Methode aus dem tatsächlichen Typ des Objekts aufgerufen.
Ein Beispiel aus dem Alltag.
Stellen Sie sich vor, Sie haben ein Team von Tieren und bitten jedes, „einen Laut von sich zu geben“. Für alle heißt der Befehl makeSound(), aber der Hund bellt, die Katze miaut und die Kuh muht. Im Code sieht das wie der Aufruf derselben Methode aus, aber das Ergebnis ist verschieden – das ist die Magie des Überschreibens!
Syntax des Überschreibens
Ein einfaches Beispiel
class Animal {
void makeSound() {
System.out.println("Das Tier gibt irgendeinen Laut von sich");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Wau!");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Miau!");
}
}
Hier ist in der Klasse Animal die Methode makeSound() definiert. Die Unterklassen Dog und Cat überschreiben diese Methode und liefern ihre eigene Implementierung.
Die Annotation @Override
In Java wird empfohlen (und es ist eine gute Gewohnheit), überschriebene Methoden mit der Annotation @Override zu kennzeichnen:
@Override
void makeSound() { ... }
Das ist für die Ausführung des Codes nicht erforderlich, aber:
- Der Compiler prüft, ob Sie die Methode tatsächlich überschreiben (und sich nicht versehentlich im Namen oder in den Parametern vertippt haben).
- Verbessert die Lesbarkeit – anderen Entwicklern ist sofort klar, dass diese Methode die der Oberklasse überschreibt.
Aufruf der überschriebenen Methode
Animal myDog = new Dog();
myDog.makeSound(); // Gibt aus: Wau!
Obwohl die Variable vom Typ Animal ist, zeigt sie tatsächlich auf ein Objekt vom Typ Dog, und aufgerufen wird die Methode aus der Klasse Dog. Das ist dynamische (späte) Bindung.
2. Unterschied zwischen Überschreiben (overriding) und Überladen (overloading)
Überladung (overloading)
- In derselben Klasse (oder in der Hierarchie, aber immer „nah beieinander“).
- Methoden haben denselben Namen, aber unterschiedliche Parameter (Typ, Anzahl, Reihenfolge).
- Die Methodenauswahl erfolgt während der Kompilierung.
void print(int x) { ... }
void print(String s) { ... }
Überschreibung (overriding)
- In verschiedenen Klassen: Die Methode ist in der Oberklasse deklariert und in der Unterklasse überschrieben.
- Methoden haben denselben Namen und dieselbe Signatur (Parameter und Rückgabetyp).
- Die Methodenauswahl erfolgt zur Laufzeit (run-time) anhand des tatsächlichen Typs des Objekts.
Vergleichstabelle
| Überladung (overloading) | Überschreibung (overriding) | |
|---|---|---|
| Wo | In derselben Klasse | In Oberklasse und Unterklasse |
| Methodenname | Gleich | Gleich |
| Parameter | Unterschiedlich | Gleich |
| Rückgabetyp | Kann abweichen | Muss identisch sein oder ein Subtyp |
| Zeitpunkt der Auswahl | Kompilierung | Laufzeit (run-time) |
| Annotation | Nicht erforderlich | @Override (empfohlen) |
3. Regeln für das Überschreiben von Methoden
Überschreiben ist mächtig, aber mit strengen Regeln verbunden. Schauen wir sie uns der Reihe nach an.
Die Methodensignatur muss übereinstimmen
- Methodenname, Typen und Reihenfolge der Parameter müssen mit der Methode in der Oberklasse identisch sein.
- Der Rückgabetyp muss identisch oder kovariant sein (das heißt ein Subtyp des Rückgabetyps der Oberklassenmethode).
Beispiel mit kovariantem Rückgabetyp:
class Animal {
Animal reproduce() { return new Animal(); }
}
class Cat extends Animal {
@Override
Cat reproduce() { return new Cat(); } // OK! Cat ist ein Subtyp von Animal
}
Zugriffsmodifizierer
Der Zugriffsmodifizierer der überschriebenen Methode darf nicht restriktiver sein als der der Methode in der Oberklasse. Ist die Methode in der Oberklasse public, muss sie in der Unterklasse ebenfalls public bleiben. Sie darf nicht weniger sichtbar gemacht werden (protected oder private sind unzulässig).
Beispiel:
class Parent {
public void greet() { }
}
class Child extends Parent {
// void greet() { } // Fehler! Der Standardmodifizierer (package-private) ist weniger sichtbar als public
@Override
public void greet() { } // OK
}
Ausnahmen
- Eine überschreibende Methode darf keine neuen Checked Exceptions werfen, die in der Basismethode nicht deklariert sind.
- Weniger oder dieselben Ausnahmen zu deklarieren ist erlaubt.
Beispiel:
class Parent {
void doWork() throws IOException { }
}
class Child extends Parent {
@Override
void doWork() throws FileNotFoundException { } // OK, FileNotFoundException ist ein Subtyp von IOException
// void doWork() throws SQLException { } // Fehler! SQLException ist in der Oberklasse nicht deklariert
}
Statische Methoden werden nicht überschrieben
Statische Methoden können verborgen (hidden) werden, aber nicht überschrieben. Wenn Sie in der Unterklasse eine statische Methode mit derselben Signatur deklarieren, ist das kein Überschreiben! Es ist lediglich ein Verbergen der Methode, kein Polymorphismus.
class Animal {
static void info() { System.out.println("Animal"); }
}
class Dog extends Animal {
static void info() { System.out.println("Dog"); }
}
Der Aufruf Dog.info() gibt "Dog" aus, doch wenn über eine Variable vom Typ Animal aufgerufen wird, wird die Methode Animal.info() aufgerufen. Das ist kein Polymorphismus!
final-Methoden können nicht überschrieben werden
Wenn eine Methode in der Oberklasse als final deklariert ist, führt der Versuch, sie zu überschreiben, zu einem Kompilierfehler.
class Animal {
final void sleep() { }
}
class Dog extends Animal {
// @Override
// void sleep() { } // Fehler! Eine final-Methode kann nicht überschrieben werden
}
4. Praktische Beispiele
Schauen wir uns in der Praxis an, wie Überschreiben funktioniert und worin der Unterschied zur Überladung besteht.
Beispiel 1: Die Klasse Shape und ihre Unterklassen
class Shape {
void draw() {
System.out.println("Wir zeichnen eine Form");
}
}
class Circle extends Shape {
@Override
void draw() {
System.out.println("Wir zeichnen einen Kreis");
}
}
class Rectangle extends Shape {
@Override
void draw() {
System.out.println("Wir zeichnen ein Rechteck");
}
}
Wir nutzen Polymorphismus:
public class Main {
public static void main(String[] args) {
Shape s1 = new Circle();
Shape s2 = new Rectangle();
s1.draw(); // Wir zeichnen einen Kreis
s2.draw(); // Wir zeichnen ein Rechteck
}
}
Obwohl die Variablen als Shape deklariert sind, wird die Methode der Klasse aufgerufen, zu der das Objekt tatsächlich gehört.
Beispiel 2: Unterschied zur Überladung
class Printer {
void print(String s) {
System.out.println("Zeichenkette: " + s);
}
void print(int n) {
System.out.println("Zahl: " + n);
}
}
Hier heißen beide Methoden print, haben aber unterschiedliche Parameter – das ist Überladung, nicht Überschreibung.
5. Überschreiben und Aufruf der Oberklassenmethode (super)
Manchmal möchte man in der überschriebenen Methode zuerst die Logik der Oberklasse ausführen und danach eigene Logik hinzufügen. Dafür wird das Schlüsselwort super verwendet.
class Animal {
void makeSound() {
System.out.println("Das Tier gibt einen Laut von sich");
}
}
class Dog extends Animal {
@Override
void makeSound() {
super.makeSound(); // Aufruf der Oberklassenmethode
System.out.println("Wau!");
}
}
Der Aufruf new Dog().makeSound() gibt aus:
Das Tier gibt einen Laut von sich
Wau!
Wie dynamische Bindung funktioniert (late binding)
Wenn Sie eine Methode über eine Referenz des Basistyps aufrufen, schaut Java zur Laufzeit nach, zu welchem realen Objekt diese Referenz gehört, und ruft genau die Version der Methode auf, die in der Klasse dieses Objekts definiert ist.
Animal a = new Cat();
a.makeSound(); // Ruft Cat.makeSound() auf, nicht Animal.makeSound()
Das ist die Grundlage des Polymorphismus in Java.
6. Wie das mit Ihrer Anwendung zusammenhängt
In unserer Übungsanwendung (zum Beispiel einem Mitarbeiterverwaltungssystem) können Sie eine Basisklasse Employee mit der Methode work() erstellen, und die Unterklassen Manager und Developer können diese Methode auf ihre Weise implementieren:
class Employee {
void work() {
System.out.println("Der Mitarbeiter arbeitet");
}
}
class Manager extends Employee {
@Override
void work() {
System.out.println("Der Manager leitet");
}
}
class Developer extends Employee {
@Override
void work() {
System.out.println("Der Entwickler schreibt Code");
}
}
Nun können Sie alle Mitarbeiter in einem Array oder in einer Liste speichern:
Employee[] employees = {new Manager(), new Developer(), new Developer()};
for (Employee e : employees) {
e.work(); // Für jeden: eigene Ausgabe!
}
7. Typische Fehler beim Überschreiben von Methoden
Fehler Nr. 1: Tippfehler im Methodennamen oder in den Parametern. Wenn Sie sich im Namen oder in den Parametern der Methode vertun, überschreiben Sie die Methode nicht, sondern erstellen eine neue. Polymorphismus funktioniert dann nicht. Verwenden Sie deshalb immer die Annotation @Override – der Compiler weist Sie sofort darauf hin.
Fehler Nr. 2: Ein restriktiverer Zugriffsmodifizierer. Wenn die Methode in der Oberklasse public ist und Sie sie in der Unterklasse als protected oder ohne Modifizierer deklarieren, erhalten Sie einen Kompilierfehler.
Fehler Nr. 3: Versuch, eine static- oder final-Methode zu überschreiben. Statische Methoden werden nicht überschrieben, und final-Methoden lassen sich gar nicht überschreiben. Wenn Sie es versuchen – der Compiler stoppt Sie.
Fehler Nr. 4: Änderung des Rückgabetyps auf einen inkompatiblen. Wenn der Rückgabetyp der Methode in der Unterklasse nicht mit dem in der Oberklasse übereinstimmt (und kein Subtyp davon ist), erlaubt der Compiler das Überschreiben nicht.
Fehler Nr. 5: Hinzufügen neuer Checked Exceptions. Eine überschreibende Methode darf keine neuen Checked Exceptions werfen, die im Deklarationskopf der Basismethode nicht enthalten sind. Andernfalls meldet der Compiler einen Fehler.
Fehler Nr. 6: super vergessen. Wenn Sie in der überschriebenen Methode einen Teil des Verhaltens der Oberklasse beibehalten möchten, vergessen Sie nicht, super.methodName() explizit aufzurufen. Java macht das nicht automatisch.
Jetzt wissen Sie, wie Methodenüberschreibung funktioniert, worin sie sich von Überladung unterscheidet und wie damit Polymorphismus in Java umgesetzt wird. In der nächsten Vorlesung betrachten wir, wie man Polymorphismus in der Praxis anwendet – mit Collections, Arrays und realen Aufgaben!
GO TO FULL VERSION