Java Method Overriding
Methoden der Elternklasse in Java-Unterklassen überschreiben – mit der @Override-Annotation und dynamischem Dispatch.
Überschreiben (Overriding) bedeutet, dass eine Unterklasse eine Methode mit derselben Signatur wie eine geerbte Methode deklariert und damit die Version der Elternklasse ersetzt. In Kombination mit Polymorphismus ist es der Mechanismus, der es ermöglicht, eine Methode über eine Referenz des Elterntyps aufzurufen und dabei die richtige Implementierung der Unterklasse auszuführen.
Dieses Kapitel legt die Regeln fest: was als Override gilt, was nicht, was der Compiler in der Unterklassenversion erlaubt zu ändern, und was @Override tatsächlich bringt.
Wie ein Override aussieht
Eine Methode überschreibt eine geerbte, wenn all das gilt:
- Gleicher Name.
- Gleiche Parameterliste (Typen und Reihenfolge nach generischer Typlöschung).
- Rückgabetyp gleich dem der Elternklasse oder ein Subtyp davon (kovariante Rückgabe).
- Sichtbarkeit gleich der Elternklasse oder weiter.
- Nicht als
final,staticoderprivatein der Elternklasse deklariert.
class Animal {
String speak() { return "(noise)"; }
}
class Cat extends Animal {
@Override
String speak() { return "meow"; }
}Das ist ein gültiger Override. Der Aufruf von speak() auf einer Cat gibt "meow" zurück; der Aufruf über eine Animal-Referenz, die auf eine Cat zeigt, gibt ebenfalls "meow" zurück — der Dispatch wird durch die Klasse des tatsächlichen Objekts bestimmt.
@Override — immer verwenden
@Override ist eine Annotation, die dem Compiler mitteilt: „Ich beabsichtige, damit eine geerbte Methode zu überschreiben." Falls dies nicht tatsächlich der Fall ist — falscher Name, falsche Parameter, falscher Rückgabetyp — gibt der Compiler einen Fehler aus:
class Cat extends Animal {
@Override
String Speak() { return "meow"; } // ERROR — no Speak() in Animal
}Ohne die Annotation würde dies stillschweigend als eine brandneue Methode namens Speak kompiliert, und ((Animal)cat).speak() würde weiterhin das "(noise)" der Elternklasse zurückgeben. Die Annotation kostet nichts und fängt eine ganze Kategorie stiller Fehler ab. Schreiben Sie sie immer bei Overrides.
Die Overrides, die man am häufigsten schreibt, betreffen nicht eigene Klassen — sondern Methoden, die von Object geerbt werden, der Wurzel jeder Klasse. Das Überschreiben von toString(), equals(Object) und hashCode() sorgt dafür, dass Objekte lesbar ausgegeben werden, wertbasiert verglichen werden und sich als Schlüssel in einer HashMap oder HashSet korrekt verhalten. @Override darauf erkennt den klassischen Fehler, equals(Cat other) (eine Überladung) statt equals(Object other) (der echte Override) zu schreiben.
Überschreiben vs. Überladen
| Aspekt | Überschreiben (Overriding) | Überladen (Overloading) |
|---|---|---|
| Wo | Zwischen Unterklasse und Oberklasse | In derselben Klasse |
| Signatur | Muss zur Elternklasse passen | Muss sich von den anderen unterscheiden |
| Dispatch | Laufzeit, basierend auf dem tatsächlichen Objekt | Kompilierzeit, basierend auf Argumenttypen |
| Annotation | @Override | keine |
Diese werden häufig verwechselt. Überschreiben ist eine Methode, mehrere Objekttypen. Überladen sind mehrere Methoden, ein Typ, ausgewählt nach dem, was übergeben wird.
Kovariante Rückgabetypen
Die Unterklasse kann einen engeren Rückgabetyp als die Elternklasse deklarieren:
class Animal {
Animal makeBaby() { return new Animal(); }
}
class Cat extends Animal {
@Override
Cat makeBaby() { return new Cat(); } // returns Cat, not Animal — fine
}Das ist Kovarianz — der Rückgabetyp des Overrides ist ein Subtyp des Elterntyps. Es ist sicher, weil jeder Aufrufer, der ein Animal von makeBaby() erwartet, problemlos eine Cat akzeptiert. Der Vorteil ist, dass Aufrufer, die wissen, dass sie eine Cat haben, ein Cat zurückbekommen, ohne zu casten:
Cat kitten = new Cat().makeBaby(); // no cast neededDer umgekehrte Weg (eine Unterklasse erweitert den Rückgabetyp) ist nicht erlaubt — das würde Aufrufer brechen, die den engeren Typ erwartet haben.
Sichtbarkeit kann nur erweitert werden
Der Override muss mindestens so sichtbar sein wie die Methode der Elternklasse:
class A { public void hi() { } }
class B extends A { protected void hi() { } } // ERROR — reduces visibility
class C extends A { public void hi() { } } // okEine Einschränkung würde bedeuten, dass ein Aufrufer mit einer A-Referenz hi() aufrufen könnte, aber nach der Zuweisung A a = new B() nicht mehr — was die Austauschbarkeit brechen würde.
Ausnahmen können nur eingeschränkt werden
Wenn die Methode der Elternklasse keine geprüften Ausnahmen deklariert, darf der Override ebenfalls keine deklarieren. Wenn die Elternklasse eine geprüfte Ausnahme wirft, kann der Override dieselbe oder eine spezifischere Unterklasse davon werfen — aber keine breitere:
class A {
void run() throws IOException { ... }
}
class B extends A {
@Override void run() throws FileNotFoundException { ... } // ok — FileNotFoundException extends IOException
}
class C extends A {
@Override void run() throws Exception { ... } // ERROR — too broad
}Ungeprüfte Ausnahmen (Unterklassen von RuntimeException) sind uneingeschränkt — sie können immer aus einem Override geworfen werden.
Was nicht überschrieben werden kann
private-Methoden. Sie sind für die Unterklasse nicht sichtbar, daher ist eine Methode mit demselben Namen in der Unterklasse schlicht eine neue Methode.final-Methoden. Explizit markiert, um Overrides zu verhindern.static-Methoden. Eine Unterklasse kann eine statische Methode mit demselben Namen und derselben Signatur deklarieren, aber das nennt sich Method Hiding, nicht Overriding. Es gibt keinen Polymorphismus — die JVM löst den Aufruf anhand des Compile-Zeit-Typs der Referenz auf:
class A { static String klass() { return "A"; } }
class B extends A { static String klass() { return "B"; } }
A a = new B();
System.out.println(a.klass()); // "A" — static, not polymorphicDies ist einer der wenigen Fälle, bei denen die Regel „Method Dispatch wählt die Version des tatsächlichen Objekts" nicht gilt.
Die Elternmethode aus dem Override aufrufen
Ein gängiges Muster ist, das Verhalten der Elternklasse mit super.method(...) zu erweitern statt zu ersetzen:
class Logger {
void log(String s) { System.out.println(s); }
}
class TimestampedLogger extends Logger {
@Override
void log(String s) {
super.log("[" + System.currentTimeMillis() + "] " + s);
}
}Der Override ergänzt das Verhalten der Elternklasse, anstatt es zu duplizieren. Dies wird im Kapitel über das super-Schlüsselwort behandelt.
Ein ausgearbeitetes Beispiel
Was kommt als Nächstes
Sie haben nun gesehen, wie Unterklassen Elternmethoden ersetzen. Die nächste Idee — Abstraktion — ist die Kehrseite: eine Methode deklarieren, die keinen Standardrumpf hat und jede konkrete Unterklasse zwingt, einen bereitzustellen. Weiter zu Java-Abstraktion.