Java-Vererbung
Klassenverhalten in Java mit dem extends-Schlüsselwort wiederverwenden und erweitern – mit den Regeln der einfachen Vererbung.
Vererbung ermöglicht es einer Klasse, auf einer anderen aufzubauen, anstatt von vorne zu beginnen. Die neue Klasse — die Unterklasse — erhält alle Felder und Methoden der Elternklasse und fügt nur das Unterschiedliche hinzu (oder ersetzt es). So drückt Java "ist-ein"-Beziehungen aus: eine Cat ist ein Animal, ein AdminUser ist ein User.
Das Schlüsselwort ist extends. Die Mechanik ist unkompliziert. Der schwierige Teil — und worum es in diesem Kapitel wirklich geht — ist zu erkennen, wann Vererbung das richtige Werkzeug ist und wann nicht.
Ein erstes Beispiel
public class Animal {
String name;
void breathe() { System.out.println(name + " breathes"); }
}
public class Cat extends Animal {
void purr() { System.out.println(name + " purrs"); }
}Cat deklariert nur purr(). Sie hat dennoch name und breathe(), weil sie diese geerbt hat:
Cat c = new Cat();
c.name = "Mittens";
c.breathe(); // Mittens breathes — inherited from Animal
c.purr(); // Mittens purrs — declared on CatAnimal ist die Superklasse (oder Elternklasse), Cat ist die Unterklasse (oder Kindklasse). Andere Sprachen nennen sie Basis-/abgeleitete Klasse.
Was vererbt wird
Eine Unterklasse erbt:
- Alle
public- undprotected-Felder und -Methoden von jedem Vorfahren. - Paket-private Felder und Methoden, wenn sich die Unterklasse im selben Paket befindet.
- Alle geerbten Member behalten ihre ursprünglichen Modifikatoren.
Eine Unterklasse erbt nicht:
- Konstruktoren. Sie sind nicht in demselben Sinne Member — die Unterklasse benötigt eigene.
private-Felder und -Methoden. Sie existieren in der Elternklasse (das Speicher-Layout einerCatumfasst weiterhin alle privatenAnimal-Felder), aber die Unterklasse kann sie nicht namentlich ansprechen. Die einzige Möglichkeit, sie zu lesen, sind geerbtepublic-/protected-Accessor-Methoden.
Einfache Vererbung — nur eine Elternklasse
Jede Klasse erweitert genau eine andere Klasse. Es gibt keine Mehrfachvererbung für Klassen in Java:
public class Hybrid extends Animal, Vehicle { } // ERROR — no multiple inheritanceMan kann viele Interfaces implementieren (behandelt in interfaces) — das bietet die Flexibilität mehrerer Quellen, die die meisten Sprachen durch Mehrfachvererbung abdecken, ohne die damit verbundenen Mehrdeutigkeitsprobleme.
Wenn man extends gar nicht schreibt, erweitert die Klasse implizit Object:
public class Foo { }
// is equivalent to
public class Foo extends Object { }Deshalb besitzt jedes Java-Objekt toString(), equals(), hashCode() und getClass() — sie alle befinden sich auf Object. Das Kapitel über die Object-Klasse behandelt sie im Detail.
Klassen, die nicht erweitert werden können
Eine als final markierte Klasse kann überhaupt keine Elternklasse sein — der Versuch, sie zu erweitern, ist ein Kompilierfehler:
public final class Money { }
public class Coupon extends Money { } // ERROR — cannot inherit from final MoneyDas ist beabsichtigt, kein Versehen: Viele Kerntypen sind genau deshalb final, damit keine Unterklasse ihr Verhalten ändern kann. String, Integer und die anderen Wrapper-Klassen sind alle final, was sie sicher zum Teilen und Cachen macht. Wenn man einen Typ möchte, der nicht durch Unterklassen erweitert werden kann — üblicherweise aus Sicherheits- oder Unveränderlichkeitsgründen — markiert man ihn als final.
Konstruktoren und super
Jeder Unterklassen-Konstruktor muss als erste Aktion einen Elternklassen-Konstruktor aufrufen. Wenn man den Aufruf nicht schreibt, fügt Java super() ein:
public class Animal {
String name;
public Animal(String name) { this.name = name; }
}
public class Cat extends Animal {
public Cat(String name) {
super(name); // call Animal(String)
}
}Wenn Animal keinen No-Arg-Konstruktor hätte und Cat kein super(...) schreiben würde, würde der Compiler sich beschweren — es gibt kein implizit einzufügendes Animal(). Das Kapitel über das super-Schlüsselwort behandelt super(...)-Konstruktorketten und super.method()-Aufrufe vollständig.
Überschreiben
Eine Unterklasse kann eine geerbte Methode ersetzen, indem sie eine mit derselben Signatur deklariert:
public class Animal {
String speak() { return "(some noise)"; }
}
public class Cat extends Animal {
@Override
String speak() { return "meow"; }
}
Cat c = new Cat();
System.out.println(c.speak()); // meow@Override ist eine Annotation, die dem Compiler mitteilt: "Diese Methode soll eine geerbte überschreiben — bitte gib einen Fehler aus, wenn das nicht der Fall ist." Man sollte sie immer verwenden. Sie fängt Tippfehler und Signaturabweichungen ab, die andernfalls stillschweigend eine neue Methode erstellen würden, anstatt die alte zu überschreiben. Das Kapitel über Methoden-Überschreiben behandelt die vollständigen Regeln.
Upcast und Polymorphismus
Eine Instanz einer Unterklasse kann einer Variablen des Elterntyps zugewiesen werden:
Animal a = new Cat(); // upcast — implicit
a.speak(); // calls Cat's speak() — picked at runtimeDies ist die Grundlage des Polymorphismus, des nächsten Kapitels. Die Variable a hat den Typ Animal, aber das tatsächliche Objekt ist eine Cat, daher wird die Cat-Version von speak ausgeführt.
Wann Vererbung das falsche Werkzeug ist
Vererbung ist der am häufigsten missbrauchte Mechanismus in der OOP. Einige Warnsignale, dass man stattdessen Komposition (ein Feld eines anderen Typs) verwenden sollte:
- Die Unterklasse besteht den "ist-ein"-Test wirklich nicht. Ein
Stackist kein wirklicherVector— dennoch erweitertjava.util.StackdenVectorund gilt weithin als Designfehler. - Die veränderbaren Interna der Elternklasse sickern in die Unterklasse durch. Änderungen an der Implementierung der Elternklasse brechen die Unterklasse.
- Man erbt, um einige Methoden wiederzuverwenden, nicht weil die Typen wirklich austauschbar sind.
Die Faustregel aus Joshua Bloch's Effective Java: Komposition der Vererbung vorziehen. Wenn B das Verhalten von A benötigt, aber kein echtes A ist, gibt man B ein privates Feld vom Typ A und leitet weiter, was gebraucht wird.
// Inheritance — fragile
public class MyList extends ArrayList<String> { ... }
// Composition — robust
public class MyList {
private final List<String> inner = new ArrayList<>();
public void add(String s) { inner.add(s); }
}Die Kompositions-Version ist immun gegen Überraschungen, wenn ArrayList neue Methoden hinzufügt oder ändert, wie ihre privaten Felder funktionieren.
Vererbung und Zugriff
Geerbte Member behalten den Modifikator, den sie in der Elternklasse hatten. Eine Unterklasse kann die Sichtbarkeit einer überschriebenen Methode nicht einschränken — eine public-Methode in der Unterklasse private zu machen würde das Liskov-Substitutionsprinzip verletzen, und der Compiler verweigert dies:
public class A {
public void hello() { }
}
public class B extends A {
private void hello() { } // ERROR — cannot reduce visibility
}Man kann die Sichtbarkeit erweitern (eine protected-Methode als public überschreiben), sollte dies aber selten tun.
Ein ausführliches Beispiel
Was kommt als Nächstes
super tauchte hier mehrmals auf — bei Konstruktorketten und als Möglichkeit, eine überschriebene Elternmethode zu erreichen. Das nächste Kapitel über das super-Schlüsselwort geht durch jede Situation, in der es vorkommt. Wenn eine Elternklasse was ihre Kinder tun sollen, aber nicht wie, definieren soll, greift man auf abstrakte Klassen zurück, die direkt auf den hier behandelten Vererbungsregeln aufbauen.