Polymorphie
Polymorphie
Polymorphie (polymorphism)
- Ein Objekt hat nur eine einzige Form, bestimmt durch seinen Typ
- Eine Variable kann sich polymorph (vielgestaltig) verhalten: sie kann auf Objekte verschiedenen Typs zeigen
- Eine Variable, vom Typ der Superklasse kann Objekte aller Subklassen referenzieren
- Es kann aber nur auf die Methoden und Attribute zugegriffen werden, die bereits in der Superklasse deklariert waren
Mitarbeiter mi = new Manager();
mi.gehalt += 100.00;
mi.abteilung = "HQX"; // FEHLER!!
Manager ma = new Mitarbeiter(); // FEHLER!!
Heide Balzert definiert Polymorphismus wie folgt:
Heide Balzert, Lehrbuch der Objektmodellierung
Der Hintergrund für die Einschränkung der aufrufbaren Methoden und Attribute auf die der Superklasse, liegt an der Typprüfung durch den Compiler. Da es sich bei Java um eine stark typisierte Sprache handelt, überprüft der Java Compiler die Methodenaufrufe oder Attributzugriffe. Der Compiler kann aber nur den Typ der Variablen sehen, nicht aber den Typ des Objekts auf den die Variable zeigt, da dieser erst zur Laufzeit feststeht. Im hier dargestellten Beispiel kann man die Objekttypen zwar aus dem Sourcecode ablesen, was aber normalerweise nicht möglich ist. Man denke nur an Aufrufe von Methoden, die einen polymorphen Parameter haben.
Beispiel: Polymorphie
Manager ma = new Manager();
Mitarbeiter mi = new Mitarbeiter();
gehaltserhoehung(0.12, ma);
gehaltserhoehung(0.02, mi);
...
public void gehaltserhoehung(double prozente,
Mitarbeiter mi) {
mi.gehalt *= 1.0 + prozente;
}
Dieses Beispiel zeigt einen sehr häufigen Fall für den Einsatz von Polymorphie: Die Methode gehaltserhoehung
benötigt nur Daten aus der Klasse Mitarbeiter, um zu funktionieren. Aufgrund der Polymorphie und der Tatsache, dass ein Manager ebenfalls ein Mitarbeiter ist, kann man die Methode aber mit Objekten vom Typ Manager aufrufen. Auch wenn man die Vererbungshierachie um weitere Klassen, wie z. B. Geschaeftsfuehrer
oder Vorstand
erweitert, die Methode gehaltserhoehung
funktioniert weiter problemlos und muss nicht angepasst werden. Dies ist eine ausgesprochene Stärke der objektorientierten Programmierung: ich kann meine Klassen weiter spezialisieren, muss aber die Methoden, die auf Objekten der Superklassen arbeiten, nicht anpassen.
Virtueller Methodenaufruf
- Polymorphie kann nur funktionieren, wenn bei überschriebenen Methoden die richtige Variante gerufen wird
- Die Entscheidung, welche Methode benutzt wird, muss zur Laufzeit anhand des Objekttyps und nicht zur Compilezeit anhand des Variablentyps erfolgen
Mitarbeiter mi = new Mitarbeiter();
Manager ma = new Manager();
System.out.println(mi.getDetails());
System.out.println(ma.getDetails());
Mitarbeiter mu = new Manager();
System.out.println(mu.getDetails());
Man spricht deshalb von einem virtuellen Methodenaufruf, weil der Compiler zum Zeitpunkt des Übersetzens nicht weiß, welche Methode letztendlich aufgerufen wird. Er kann daher noch gar meinen „echten“ Methodenaufruf generieren, sondern muss Code erzeugen, der dann zur Laufzeit den Typ feststellt und die richtige Methode ruft. Der vom Compiler erzeugte Methodenaufruf ist also gar kein echter Methodenaufruf, sondern eben nur „virtuell“.
Statischer vs. dynamischer Typ
Eine Variable hat einen
- statischen Typ (static type) - Typ mit dem sie deklariert wurde
- Bestimmt, welche Methoden und Felder sichtbar sind
- Wird beim Compilieren festgelegt und ändert sich nicht mehr
- dynamischen Typ (dynamic type) - Typ des referenzierten Objekts
- Bestimmt welche Methoden letztendlich aufgerufen werden
- Kann sich mit jeder Zuweisung ändern
Mitarbeiter mi = new Manager(); // statisch: Mitarbeiter, dynamisch: Manager
mi = new Mitarbeiter(); // statisch: Mitarbeiter, dynamisch: Mitarbeiter
Der statische Typ wird häufig Compile-Zeit-Typ (compile time type) und der dynamische Laufzeit-Typ (runtime type) genannt.
Die Klasse Object
- Die Klasse
Object
ist die Wurzel aller Vererbungshierarchien - Jede Java-Klasse erbt direkt oder indirekt von
Object
- Wenn eine Klasse von keiner anderen erbt, erzeugt der Compiler automatisch ein
extends Object
, d. h. auspublic class Klasse {}
wirdpublic class Klasse extends Object {}
- Die Methoden von
Object
werden später behandelt
Die Methoden der Klasse Object
werden im Folgenden Kapitel behandelt. An dieser Stelle interessieren wir uns nur für die Nutzung von Object
im Rahmen der Polymorphie.
Object als der universelle Variablentyp
Da Object
die Wurzel aller Klassenhierarchien ist, kann eine Variable vom Typ Object
Referenzen auf Objekte jedes beliebigen Typs aufnehmen
Object o = new Manager();
o = new String();
Object[] os = new Object[2];
os[0] = new Manager();
os[1] = new Date();
Referenzen vom Typ Object
sind aufgrund der Polymorphie die universalen Referenzen, da Object
die Wurzel aller Vererbungshierarchien ist. Sie entsprechen damit ein wenig dem void-Pointer (void*
) von C, der auch auf jedes beliebige Objekt (im nicht objektorientierten Sinne) auf dem Heap zeigen kann. Natürlich kann man bei der Verwendung von Object
-Referenzen dann ohne Cast auf keine Felder oder Methoden der dahinter liegenden Objekte zugreifen, da man durch die „Brille“ von Object
auf die Objekte schaut.
Liskovsches Substitutionsprinzip
Barbara Liskov, Data Abstraction and Hierarchy, SIGPLAN Notices, 23,5 (May, 1988)

Das liskovsche Substitutionsprinzip lautet übersetzt
Man kann mit Vererbung natürlich problemlos Vererbungshierarchien bauen, die das liskovsche Substitutionsprinzip verletzen, z. B. indem man von einer Klasse Rechteck die Klasse Quadrat ableitet und dann in der Klasse Quadrat einen Fehler wirft, wenn jemand versucht unterschiedliche Werte für die beiden Seitenlängen anzugeben. Obwohl es sich in diesem Fall um ein vollkommen korrektes Konstrukt handelt, das vom Compiler akzeptiert wird, verletzt es das liskovsche Substitutionsprinzip mit der Folge, dass Quadrat kein Subtyp im liskovschen Sinne von Rechteck ist.
Wenn man Klassen und Vererbungshierarchien so baut, dass das liskovsche Substitutionsprinzip eingehalten wird, sind die Programme robuster und besser zu verstehen. Andernfalls vererbt man zwar Implementierung und Daten, aber es entsteht keine vollständige ist-eine Beziehung.
Casting
- Durch einen Cast gibt man explizit den dynamischen Typ an. Es handelt sich um einen Operator. Syntax:
(TYP)
- Methoden und Attribute werden entsprechend des angegebenen Typs sichtbar
- Casts werden erst zur Laufzeit überprüft (⇒ Laufzeitfehler möglich)
Mitarbeiter mi = new Manager();
// Down-Cast
Manager ma = (Manager) mi;
ma.abteilung = "Buchhaltung";
// Down-Cast in kompakter Form
((Manager) mi).abteilung = "Buchhaltung";
Der instanceof Operator
- Manchmal muss man zur Laufzeit feststellen können, welchen Typ ein Objekt hat, z. B. um sicher casten zu können
- Der
instanceof
-Operator prüft den Typ einer Variable a instanceof T
lieferttrue
, wenn das Objekt mit der Referenz a vom Typ T oder wenn das Objekt hinter a vom Typ einer Subklasse von T istnull instanceof T
ist immerfalse
- wenn
a instanceof T
true
liefert, wird ein Cast von a auf T ((T) a
) niemals fehlschlagen (keine Ausnahme werfen)
Mitarbeiter mi = new Manager();
(mi instanceof Mitarbeiter) --> true
(mi instanceof Manager) --> true
(mi instanceof Geschaeftsfuehrer) --> false
(null instanceof Mitarbeiter) --> false
Der instanceof
Operator liefert für alle Subklassen true
zurück, d. h. man kann ihn nicht verwenden, wenn man sicher sein will, dass ein Objekt exakt von einem bestimmten Typ ist. Hierzu gibt es aber im Rahmen von Reflection Möglichkeiten, die aber in der Realität nicht häufig benutzt werden.
Wichtig ist anzumerken, dass man instanceof
mit Nullreferenzen verwenden darf. Hier ist dann per Definition das Ergebnis immer false
. Anstatt if (s != null && s instanceof String)
kann man kürzer if (s instanceof String)
schreiben.
public void automatischeErhoehung(Mitarbeiter mi) {
if (mi instanceof Geschaeftsfuehrer) {
Geschaeftsfuehrer g = (Geschaeftsfuehrer) mi;
if (g.prokura) {
mi.gehalt = mi.gehalt * (1 + 0.2);
} else {
mi.gehalt = mi.gehalt * (1 + 0.1);
}
} else if (mi instanceof Manager) {
mi.gehalt = mi.gehalt * (1 + 0.05);
} else {
mi.gehalt = mi.gehalt * (1 + 0.002);
}
}
Dieses Beispiel zeigt, wie man mithilfe des instanceof-Operators abhängig vom dynamischen Typs des übergebenen Objekts, unterschiedliche Aktionen durchführen kann und wie man einen Cast (hier auf Geschaeftsfuehrer
) durch eine entsprechende Prüfung mit instanceof
schützen kann.
Es ist aber anzumerken, dass längere Ketten von if/else if-Konstrukten mit instanceof
als schlechter Programmierstil gelten und daher vermieden werden sollten. Der Grund hierfür ist, dass ein solcher Stil anfällig für Fehler ist, die durch Veränderungen an der Vererbungshierarchie entstehen:
Wenn eine weitere Klasse hinzugefügt wird, muss man gewährleisten, dass ein passendes if
eingebaut wird.
Weiterhin müssen die Tests immer in der richtigen Reihenfolge durchgeführt werden, nämlich von unten nach oben die Vererbungshierarchie herauf. Macht man hier einen Fehler, greift schon ein weniger spezieller Fall, als erwartet.
Spätes Binden
Spätes Binden (late binding)
- Damit der virtueller Methodenaufruf funktioniert, darf die Entscheidung, welche Methode gerufen wird, erst zur Laufzeit getroffen werden
- Der Compiler kann diese Entscheidung nicht treffen, sondern muss Code generieren, der dies zur Laufzeit anhand des dynamischen Typs der Variable durchführt
Das späte Binden wird häufig als dynamisches Binden bezeichnet.
Bei Java generiert der Compiler, im Gegensatz zu anderen Sprachen, allerdings gar keinen besonderen Code für das späte Binden, sondern erzeugt nur einen besonderen Byte-Code namens invokevirtual
und überlässt dann die Magie des dynamischen Bindens der Java Virtual Machine.
Polymorphie und Überschreiben
Beim Überschreiben von Methoden
- darf der Rückgabetyp nicht verändert werden
- darf die Sichtbarkeit nicht eingeschränkt werden
- dürfen nur Exceptions weggelassen oder durch speziellere Exceptions (Subklassen) ersetzt werden
Warum?
- Der Verwender darf nicht überrascht werden
- Was mit der Superklasse geht, muss auch mit der Subklasse funktionieren
Der Grund für diese Regeln liegt in der Polymorphie, da der Verwender einer Variablen nicht damit rechnen kann, dass das Objekt ein anderes Verhalten zeigt, als der statische Typ der Variable suggeriert. Daher darf man eine Klasse, die man ableitet, nur so verändern, dass der Verwender nicht merken kann, ob er mit der Super- oder Subklasse hantiert. Sogar der instanceof-Operator ist so konstruiert, dass er bei Subklassen noch true
liefert. Andernfalls würde man nämlich den Verwender überraschen.
Wie bereits erwähnt, sind diese technischen Anforderungen relativ schwach verglichen mit dem likovschen Substitutionsprinzip, das eine deutlich höhere Anforderung an das Verhalten von überschriebenen Methoden und abgeleiteten Klassen stellt.
Im Prinzip handelt es sich bei diesen Regeln nur um die technische Ausgestaltung der folgenden Bedingung:
- Die Vorbedingung (precondition) einer Methode einer abgeleiteten Klasse darf nur schwächer als die der Superklasse sein
- Die Nachbedingung (postcondition) einer Methode einer abgeleiteten Klasse darf nur stärker sein als die der Superklasse
Polymorphie und Überladen
- Polymorphie wirkt sich nicht auf überladene Methoden aus
- Compiler wählt die überladene Methode aus, nicht die Laufzeit
public class UeberladenUndPolymorphie {
public void method(A a) {
System.out.println("method(A a) gerufen");
}
public void method(B b) {
System.out.println("method(B b) gerufen");
}
}
UeberladenUndPolymorphie u = new UeberladenUndPolymorphie();
A a1 = new A();
A a2 = new B();
u.method(a1);
u.method(a2);
Ausgabe
method(A a) gerufen
method(A a) gerufen
Die Annahme, dass Polymorphie sich auf überladene Methoden bezieht, ist falsch und trotzdem häufig anzutreffen.
Bei überladenen Methoden sucht der Compiler beim Übersetzen der Klassen nach einer Methode, deren Parameterliste und Parametertypen am besten zu dem gefunden Methodenaufruf passen und generiert dann einen entsprechenden Bytecode, der die gefundene Methode aufruft. Der Compiler kann aber nur die statischen Typen sehen, d. h. die Typen der Referenzvariablen, nicht aber die dynamischen Typen (d. h. die Typen der Objekte, die von den Variablen referenziert werden). Somit ist es nicht möglich bei überladenen Methoden ein Verhalten ähnlich der Polymorphie zu erhalten, das sich an den dynamischen Typen orientiert. Die Auswahl der überladenen Methode findet immer zur Übersetzungszeit statt.
Typischer Fehler bei Konstruktoren
Auf keinen Fall sollte man Methoden, die überschrieben werden können, vom Konstruktor aus aufrufen
public class A {
protected String name;
public A() {
name = getDetails();
}
protected String getDetails() {
return "Hans";
}
}
public class B extends A {
protected String info = "";
protected String getDetails() {
return name + info.length();
}
}
Hier führt jetzt new B();
zu einer NullPointerException
Unter „Methoden, die überschrieben werden können“ fallen alle nicht nicht-finalen (wird später noch erläutert) Methoden, die mindestens default-Sichtbarkeit haben. Diese Regel greift also nicht für private Methoden oder für Methoden, die eine größere Sichtbarkeit als private haben aber final sind. Aus diesem Grund, sollte man beim Aufruf von Setter-Methoden aus Konstruktoren vorsichtig sein, da diese sehr häufig überschreibbar sind.
Das Problem lässt sich relativ einfach lösen, indem man die Funktionalität aus der Methode getDetails
in eine private Methode auslagert und diese dann aus dem Konstruktor und der getDetails
-Methode benutzt. Die Lösung sähe dann wie folgt aus:
public class A {
protected String name;
public A() {
name = getDetailsInternal();
}
private final String getDetailsInternal() {
return "Hans";
}
protected String getDetails() {
return getDetailsInternal();
}
}
public class B extends A {
protected String info = "";
public B() {
}
protected String getDetails() {
return name + info.length();
}
public static void main(String[] args) {
B b = new B();
System.out.println(b.getDetails());
}
}