1
Polymorphismus
Korrigiert nach Anweisung von Dipl.-Ing. Dr. Steinparz
0. Inhaltsverzeichnis
0. Inhaltsverzeichnis
1.
Was ist Polymorphismus?
2. Dynamische und statische Bindung
3. Virtuelle Methoden
4. Sinn von Polymorphismus und virtuellen Methoden
5. Konstruktoren und Destruktoren
6. Zuweisungen
7.
Dynamic Cast
8. Speicherplatzreservierung
9. Redefinition oder Überladung
10. Gefahren des Polymorphismus
Anhang: Java-Codebeispiel
1. Was ist Polymorphismus?
Das Phänomen, das die gleichen Aufrufschnittstellen zu verschiedenen Methoden führen, wird dann Polymorphismus genannt, wenn die Entscheidung, welche Methode aktiv wird, zur Laufzeit erfolgt.
2.
Dynamische und statische Bindung
Polymorphismus bringt dynamische Bindung. Nicht wie sonst üblich entscheidet der Compiler zur Übersetzungszeit welche Methode aktiviert wird (statische Bindung). Zur Laufzeit wird festgestellt welchen Typ das Objekt hat und dann wird die entsprechende Methode aufgerufen.
Statische Bindung bedeutet, dass die Bindung vom Namen einer Funktion oder Methode zur deren Definition statisch, d.h. zur Übersetzungszeit erfolgt.
Dynamische Bindung bedeutet, dass der Bezug vom Namen zur Definition (die Bindung des Namens) dynamisch, d.h. zur Laufzeit, erfolgt.
Es ist klar, dass die Flexibilität der dynamischen Bindung mit komplexerem Maschinencode und leicht erhöhter Laufzeit bezahlt werden muss. Der Compiler generiert Code, der den aktuellen Typ eines Objekts feststellt und dann in die entsprechende Methode verzweigt.
Die Objekte enthalten die Adressen ihrer Methoden in einer Methodentabelle.
Der Compiler erzeugt keinen Sprung zu der Methode, sondern einen Sprung zu der Methode, deren Adresse in der Methodentabelle gefunden wird.
Polymorphismus entfaltet seine Wirkung also nur in Kombination mit Referenzen. Ein Zeiger vom Typ der Basisklasse kann auch auf ein Objekt der abgeleiteten Klasse zeigen. In dem Fall wird die Methode aktiviert, die tatsächlich zu dem Objekt gehört.
3. Virtuelle Methoden
Virtuelle Methoden sind zur Redefinition vorbereitete Methoden.
Sie werden in der Basisklasse definiert und stehen darum in jeder Ableitung zur Verfügung. Die Ableitungen können die Methoden nach eigenen Bedürfnissen umdefinieren.
Das Schlüsselwort virtual zeigt an, dass die entsprechende Methode in einer Ableitung neu definiert werden kann.
Bei einer virtuellen Methode wird zur Laufzeit der Typ eines Objekts festgestellt und dann die richtige Methode aktiviert. Enthält eine Klasse virtuelle Methoden, dann sind ihre Objekte polymorph (vielgestaltig). Die aktuelle “Gestalt”, der Typ des Objekts, wird zur Laufzeit festgestellt.
Dazu muss das Objekt eine Typmarkierung enthalten.
Bei virtuellen Methoden entscheidet der Typ des Objekts über die aufgerufene Methode.
4. Sinn von Polymorphie und virtuellen Methoden
Welchen Sinn machen nun virtuelle Methoden?
Dadurch, dass Entscheidungen zur Laufzeit getroffen werden, macht Polymorphismus Programmstücke flexibler und vielseitiger einsetzbar. Er ergänzt in dieser Hinsicht den Vererbungsmechanismus in ganz wesentlicher Art.
Vererbung bringt Flexibilität in die Programme: Zeiger auf Objekte können gespeichert werden ohne dass man sich vorher auf einen genauen Typ festlegen muss.
Polymorphismus erweitert diese Flexbilität: Eine Methode wird aufgerufen, ohne dass man an der Aufrufstelle genau wissen muss, welche das sein wird.
5. Konstruktoren und Destruktoren
Für Konstruktoren und Destruktoren gilt die Regel:
- Destruktoren können bei Bedarf virtuell sein.
- Konstruktoren können niemals virtuell sein.
Ein Konstruktor dient dazu ein Objekt mit bekanntem Typ zu initialisieren. Der Typ ist dabei immer statisch – zur Übersetzungszeit – bekannt.
Objekte werden ja nur auf Veranlassung des Programms erzeugt und dabei wird der exakte Typ angegeben.
Bei der Vernichtung von Objekten ist die Lage etwas anders. Räumt man Objekte mit weg, dann erfolgt der Zugriff über einen Zeiger. Der Zeiger kann dabei formal auf ein Objekt der Basisklasse, tatsächlich aber auf ein Objekt einer abgeleiteten Klasse zeigen. Zur Übersetzungszeit ist im Allgemeinen nicht bekannt, auf was er tatsächlich zeigt.
6.
Zuweisungen
Ein Zuweisungsoperator kann zwar virtuell definiert werden, die Zuweisung kann aber trotzdem nicht polymorph sein, da polymorphe Funktionen immer die gleichen Argumenttypen haben müssen.
7. Dynamic Cast
Mit Dynamic Casts kann zwischen Klassen in einer gemeinsamen Ableitungshierarchie konvertiert werden, wenn die Ableitungshierarchie mindestens eine virtuelle Methode enthält.
Die Ableitungshierarchie muss deswegen eine virtuelle Methode enthalten, weil nur bei Klassen mit virtuellen Methoden die Notwendigkeit besteht, Objekten dieser Klassen zur Laufzeit eine Markierung anzuheften, die Auskunft über den Typ gibt.
Ohne virtuelle Methoden fehlt die Markierung und die dynamische Typkonversion kann nicht ausgeführt werden.
8.
Speicherplatzreservierung
Speicherplatz für beliebige Objekte wird stets statisch, auf Basis des zur Übersetzungszeit bekannten Typs angelegt. Variablen eines Basistyps können darum immer nur Objekte vom Basistyp aufnehmen.
Will man mit Objekten diverser abgeleiteter Typen arbeiten, dann müssen diese über Zeiger angesprochen werden.
9. Redefinition oder Überladung
Das Schlüsselwort virtual zeigt an, dass die Entscheidung über die aufgerufene Methode zur Laufzeit, auf Basis des aktuellen tatsächlichen Typs des Objekts, erfolgt. Wird das Schlüsselwort weggelassen, dann wird aus dem Polymorphismus eine einfache Redefinition.
Im Gegensatz dazu, entscheidet bei der Überladung der Typ der Parameter darüber, welche Funktion oder Methode genau aufgerufen wird.
Überladung und Redefinition sind kein Polymorphismus, denn sie werden während der Übersetzungszeit durchgeführt.
10. Gefahren des Polymorphismus
Eine besondere Gefahrenquelle liegt darin, polymorphe Methoden im Konstruktor einer Klasse aufzurufen. Der Grund liegt in der Initialisierungsreihenfolge von Membervariablen während der Konstruktion eines Objekts:
Zuerst werden die Konstruktoren der Vaterklassen aufgerufen und so deren Membervariablen initialisiert.
Dann werden die Initialisierer und initialisierenden Zuweisungen der eigenen Klasse aufgerufen.
Schließlich wird der Rumpf des eigenen Konstruktors ausgeführt.
Wird nun im eigenen Konstruktor eine Methode aufgerufen, die in einer abgeleiteten Klasse überlagert wurde, sind die Membervariablen der abgeleiteten Klasse noch nicht initialisiert. Ihr Konstruktor wird ja erst später aufgerufen. Das kann zu schwer auffindbaren Fehlern führen. Aufrufe von Methoden, die möglicherweise überlagert werden, sollten daher im Konstruktor vermieden werden.
Anhang: Java-Codebeispiel
Wir wollen uns ein Beispiel ansehen, um diese Ausführungen zu verdeutlichen.
Zum Aufbau einer Mitarbeiterdatenbank soll zunächst eine Basisklasse definiert werden, die jene Eigenschaften implementiert, die auf alle Mitarbeiter zutreffen, wie beispielsweise persönliche Daten oder der Eintrittstermin in das Unternehmen. Gleichzeitig soll diese Klasse als Basis für spezialisierte Unterklassen verwendet werden, um die Besonderheiten spezieller Mitarbeitertypen, wie Arbeiter, Angestellte oder Manager, abzubilden. Da die Berechnung des monatlichen Gehalts zwar für jeden Mitarbeiter erforderlich, in ihrer konkreten Realisierung aber abhängig vom Typ des Mitarbeiters ist, soll eine abstrakte Methode monatsBrutto in der Basisklasse definiert und in den abgeleiteten Klassen konkretisiert werden.
Das folgende Listing zeigt die Implementierung der Klassen Mitarbeiter, Arbeiter, Angestellter und Manager zur Realisierung der verschiedenen Mitarbeitertypen. Zusätzlich wird die Klasse Gehaltsberechnung definiert, um das Hauptprogramm zur Verfügung zu stellen, in dem die Gehaltsberechnung durchgeführt wird. Dazu wird ein Array ma mit konkreten Untertypen der Klasse Mitarbeiter gefüllt (hier nur angedeutet; die Daten könnten zum Beispiel aus einer Datenbank gelesen werden) und dann für alle Elemente das Monatsgehalt durch Aufruf von monatsBrutto ermittelt.
import java.util.Date;
abstract class Mitarbeiter
{
int persnr;
String name;
Date eintritt;
public Mitarbeiter()
{
}
public abstract double monatsBrutto();
}
class Arbeiter
extends Mitarbeiter
{
double stundenlohn;
double anzahlstunden;
double ueberstundenzuschlag;
double anzahlueberstunden;
double schichtzulage;
public double monatsBrutto()
{
return stundenlohn*anzahlstunden+
ueberstundenzuschlag*anzahlueberstunden+
schichtzulage;
}
}
class Angestellter
extends Mitarbeiter
{
double grundgehalt;
double ortszuschlag;
double zulage;
public double monatsBrutto()
{
return grundgehalt+
ortszuschlag+
zulage;
}
}
class Manager
extends Mitarbeiter
{
double fixgehalt;
double provision1;
double provision2;
double umsatz1;
double umsatz2;
public double monatsBrutto()
{
return fixgehalt+
umsatz1*provision1/100+
umsatz2*provision2/100;
}
}
public class Gehaltsberechnung
{
private static final int ANZ_MA = 100;
private static Mitarbeiter[] ma;
private static double bruttosumme;
public static void main(String[] args)
{
ma = new Mitarbeiter[ANZ_MA];
//Mitarbeiter-Array füllen, z.B.
//ma[0] = new Manager();
//ma[1] = new Arbeiter();
//ma[2] = new Angestellter();
//..
.
//Bruttosumme berechnen
bruttosumme = 0.0;
for (int i=0; i<ma.length; ++i) {
bruttosumme += ma[i].monatsBrutto();
}
System.out.
println("Bruttosumme = "+bruttosumme);
}
}
Unabhängig davon, ob in einem Array-Element ein Arbeiter, Angestellter oder Manager gespeichert wird, führt der Aufruf von monatsBrutto dank der dynamischen Methodensuche die zum Typ des konkreten Objekts passende Berechnung aus. Auch weitere Verfeinerungen der Klassenhierarchie durch Ableiten neuer Klassen erfordern keine Veränderung in der Routine zur Berechnung der monatlichen Bruttosumme.
So könnte beispielsweise eine neue Klasse GFManager (ein Manager, der Mitglied der Geschäftsführung ist) aus Manager abgeleitet und problemlos in die Gehaltsberechnung integriert werden:
public class GFManager
extends Manager
{
double gfzulage;
public double monatsBrutto()
{
return super.monatsBrutto()+gfzulage;
}
}
Anmerkungen: |
| impressum | datenschutz
© Copyright Artikelpedia.com