Vererbung Grundlagen
1. Prinzip
Vererbung ist ein etwas irreführender Begriff für ein Konzept, das es erlaubt, Spezialisierungen von allgemeineren Klassen vorzunehmen (irreführend deshalb, weil z.B. biologische Merkmalsvererbung normalerweise zwischen individuellen Objekten passiert und nicht zwischen Klassen, wie es in den meisten OO-Sprachen realisiert ist).
Die Idee ist, dass z.B. in einer Schulverwaltung Schüler, Lehrer, Eltern und Verwaltungsmitarbeiter einiges gemeinsam haben - alle sind Menschen, die Name, Adresse, Sozialversicherungsnummer, E-Mail-Adresse etc. haben. Es wäre sehr praktisch, diese gemeinsamen Aspekte nicht für jeden Typ separat zu implementieren.
Genau dies ist mit Vererbung möglich - die spezialisierte Klasse, "Unterklasse", "Subclass", enthält in der Klassendeklaration eine Referenz auf die "Elternklasse", "Superclass":
public class SomeSubClass extends ItsSuperClass { … }
und alle mit Sichtbarkeit public
oder protected
deklarierten Methoden und sogar Eigenschaften der Elternklasse können in der Unterklasse/Kindklasse verwendet werden, als wären sie direkt hier implementiert.
2. Polymorphie
Das Konzept ist aber so flexibel, dass solche Methoden der Elternklasse auch neu definiert werden können – sie werden bei Bedarf in der Kindklasse neu implementiert. Dies wird als Überschreiben/Überdecken/Überlagern/Override bezeichnet. Dabei muss klarerweise die Methoden-Signatur (Methodenname + geordnete Liste der Parameter-TYPEN) übereinstimmen.
Da die Vererbung eine Spezialisierung ist, kann z.B. einer Variablen des Typs Person
auch ein Objekt der Klasse Schueler
zugewiesen werden (dieser ist ja "nebenbei" auch eine Person
).
Es kann also erst zur Laufzeit entschieden werden, welche Methodenimplementation die "nächstgelegene" und damit zu verwenden ist (Dynamisches Binden/Spätes Binden/Late Binding).
Diese Möglichkeit, auch Objekte bestimmter anderer, "abgeleiteter" Klassen anstelle exakt passender Objekte zuzuweisen und dynamisch deren passendste Methodenimplementation zu verwenden, nennt man Polymorphie.
Anders ausgedrückt ist Polymorphie die Möglichkeit, die selbe Methodensignatur in verschiedenen Stellen der Vererbungshierarchie (auch über Interfaces) zu implementieren, wobei die Auswahl, welche Version aufgerufen wird, erst zur Laufzeit erfolgt.
Dadurch wird es möglich, Methoden in Unterklassen neu, spezialisierter zu definieren – es wird die in der Hierarchie am nächsten liegende Version genutzt.
Natürlich ist es wichtig, dass die Grundidee der Methode, die sich hoffentlich im Namen ausdrückt und in der Dokumentation sowie letztlich im realen Verhalten beschrieben bzw. erkennbar ist, gewahrt bleibt – z.B. würde eine Methode calcUmfang()
oder calcFlaeche()
für unterschiedliche Figuren dennoch die passende mathematische Idee umsetzen, wenn auch mit unterschiedlichen Formeln.
Wichtig: Überladen bedeutet im Gegensatz dazu: weitere Methode mit gleichem Namen, aber unterschiedlicher Parametertyp-Folge - z.B. mehrere Konstruktoren.
Man kann das Klassenkonzept auch als Vertrag über das Vorhandensein bestimmter Methodensignaturen verstehen – wenn ich eine Variable vom Typ Person definiere, fordere ich, dass der dabei definierte Satz an Methoden auch verfügbar ist (das ist definitiv auch bei allen Kindklassen, Enkelklassen, etc. gesichert) – sonst würde ein Programmfehler auftreten.
3. Umsetzung in Java
In Java-Syntax wird die Klassendeklaration erweitert wie folgt:
public class Schueler extends Person { ... }
Auch hier ist das Schlüssenwort extends
irreführend - es ist eine Spezialisierung, erweitert wird allenfalls der Satz an Eigenschaften und Methoden, aber nicht die Idee der Klasse.
Der Konstruktor MUSS als erste Anweisung den Aufruf eines Konstruktors der Superklasse enthalten, um die korrekte Initialisierung der Instanzvariablen der Superklasse durchzuführen, die danach in den Subklassen-Konstruktoren und -Methoden korrekt verwendet werden können.
Wenn allerdings die Superklasse einen Default-Konstruktor hat, wird dieser Aufruf beim Kompilieren automatisch auch ohne expliziten Aufruf ausgeführt. Das ist problematisch!
Wenn die Superklasse keinen einzigen Konstruktor definiert hat, wird beim Kompilieren ein leerer Standardkonstruktor erstellt. Somit ist in diesem Fall ebenfalls ein implizit aufrufbarer Konstruktor der Superklasse verfügbar. Es ist allerdings – u.a. auch aus diesem Grund – schlechter Programmierstil, keine expliziten Konstruktoren zu definieren: es verdeckt u.U. das unbeabsichtigte Fehlen eines Super-Konstruktor-Aufrufs.
Wenn Schueler
nur eine einzige Methode zusätzlich benötigt:
public String confess() { /* ...*/ }
dann sieht die Klassenimplementation folgendermaßen aus:
public class Schueler extends Person {
private String schulKl;
public Schueler(String nachname, String vorname, int gebJahr, char geschl, String schulKl) {
// Aufruf d. passenden Konstr. d. Superklasse (muss 1. Statement sein!):
super(nachname, vorname, gebJahr, geschl);
this.schulKl = schulKl;
}
public String getSchulKl() {
return this.schulKl;
}
public String confess() {
System.out.println("Mein Lehrer weiß alles besser!");
}
}
Die Verwendung könnte aber so aussehen:
// ...
Person evi = new Schueler("Huber", "Evi", 2002, 'w', "2ghif");
System.out.println("Nachname: " + evi.getNachname());
System.out.println("Vorname: " + evi.getVorname());
System.out.println("Schulklasse: " + evi.getSchulKl());
System.out.println("Bekennt: " + evi.confess());
// ...
Beispiel-Eclipse-Projekt: Demo-2xHIF-02_Pkg-Main-Vererbg_2021-09-23