Eigentlich haben wir Polymorphie schon in vielen anderen Beispielen verwendet, ohne etwas davon mitzubekommen. In dieser Lektion werden wir uns aber noch einmal bewusst damit auseinander setzen.
Definition
Polymorphie bedeutet Vielgestaltigkeit und besagt in ihrem einfachsten Fall, dass Funktionen nach außen hin gleich aussehen (die gleiche Signatur haben), aber unterschiedliches Verhalten implementieren.
Dynamisches Binden
Am besten wir betrachten das folgende Beispiel:
Es existiert eine kleine Vererbungshierarchie: verschiedene Tiere erben von der Klasse Lebewesen
. Objekte dieser Klassen werden in ein Array gepackt. Hierbei ist es wichtig zu bemerken, dass es sich um ein Array vom Typ Array<Lebewesen>
handelt!
Die Funktion geräuschMachen
wird aber nicht anhand ihrer Implementierung in der Klasse Lebewesen
ausgeführt (was man ja durchaus vermuten könnte, das das Array ja Objekte vom Typ Lebewesen
enthält und nicht deren Subklassen), sondern anhand der Implementierung, welche in der Klassenhierarchie ganz unten steht.
Alle Funktionen haben die gleiche Signatur, es wird aber erst zur Laufzeit bestimmt, welche Implementierung der Funktion für das Objekt verwendet wird. Dies nennt man dynamisches Binden. Polymorphie ist also immer mit Vererbung kombiniert. Insbesondere bei der Verwendung von abstrakten Klassen oder Interfaces.
Überschreiben von Funktionen
Wenn man in einer Klassenhierarchie eine Funktion aus einer der Superklassen überschreibt, erhählt man automatisch eine polymorphe Implementierung. In Kotlin ist hierfür das Schlüsselwort override
notwendig.
Überladen von Funktionen
Eine abgeschwächte Form der Polymorphie kann mit dem Überladen von Funktionen erreicht werden. Vom Überladen spricht man, wenn Funktionen zwar den gleichen Namen tragen, aber unterschiedliche Parameter (-Typen und/oder -Anzahl) aufweisen. Welche der Funktionsimplementierungen dann tatsächlich verwendet wird, entscheidet der Compiler.
In diesem Beispiel sehen wir, wie die Funktion sagHallo()
in verschiedenen Implementierungen verschiedene Verhaltensweisen, je nach Kontext, zeigt.
Hinweis: Der Rückgabetyp kann nur Überladen werden, wenn die Signatur der Funktion sich ändert. Das hier ist NICHT erlaubt:
Vararg
Das Überladen ist in Kotlin oft ein Kinderspiel. So kann man das obige Beispiel, bei dem zwei gleichartige Parameter(name1
und name2
) entgegen genommen werden einfach abkürzen mit dem Schlüsselwort vararg
, welches dann aus dem entsprechenden Parameter einfach ein Array macht:
Initialisierungsreihenfolge
Dieser Hinweis hätte auch in früheren Kapiteln gut gepasst, ist zum jetzigen Zeitpunkt aber sicherlich besser verständlich. Bei Vererbungshierarchien muss man manchmal genau nachdenken, was an diesem Beispiel verdeutlicht werden soll:
Eigentlich würde man erwarten, dass die Ausgabe Ich bin Bello erscheint. Anstelle dessen erscheint Ich bin null. Dies liegt daran, dass als aller erstes bei der Erstellung eines neuen Objekts der Konstruktor der Superklasse aufgerufen wird. Das bedeutet, dass wenn die Zeile println("Ich bin ${nameGeben()}")
ausgeführt wird, die Property name
noch garnicht zugewiesen wurde,
Stoppuhren
Im Lektionen Projekt finden Sie das Unterprojekt Stoppuhren.
Wir wollen dort 3 verschiedene Stoppuhren implementieren. Eine TextStoppuhr
, eine DigitalStoppuhr
und eine AnalogStoppuhr
. Öffnen Sie die Main.kt
und machen Sie sich mit dem Aufbau vertraut. Dieser ist sehr einfach: Es wird ein value stoppuhr
mit folgendem Code erzeugt und bei der engine registriert:
val stoppuhr : Stoppuhr = TextStoppuhr()
register(stoppuhr)
stoppuhr.start() // Startet die Stoppuhr
Der value stoppuhr
ist vom Typ Stoppuhr
.
1. Polymorphe Funktion start()
Betrachten Sie die beiden start()
Funktionen in der Datei Stoppuhr.kt
und verstehen Sie, was hier passiert und wie hier Polymorphie verwendet wird und welchen Vorteil dies hier bietet. Versuchen Sie dies tatsächlich verbal zu formulieren.
2. TextStoppuhr
Die einfachste Stoppuhr ist die Textstoppuhr. Hier soll die gestoppte Zeit einfach in Form von einzelnen Textfeldern visualisiert werden. Dies könnte dann so aussehen:
Öffnen Sie hierzu die Datei Textstoppuhr.kt
und implementieren Sie die folgenden Schritte:
- Initialisieren Sie die values
hundertstelText
,sekundenText
undminutenText
als Objekte vom TypText
(Doku hier). Platzieren Sie die Elemente so auf dem Bildschirm, dass Sie eine Stoppuhr optisch nachempfinden. - Fügen Sie im
init-Block
die initialisierten Text-Objekte mit Hilfe der MethodeaddChild(CanvasElement)
ein. Hinweis: Die Stoppuhr ist selbst einCanvasElement
und kann mit Hilfe vonaddChild()
weitereCanvasElemente
in sich aufnehmen. Alle Operationen, welche dann auf dem Mutter-Objekt durchgeführt werden, können so auf die Kinder übertragen werden. - Ergänzen Sie die Methode
onEveryFrame()
(Doc) so, dass der Text nach den entsprechenden Zeiteinheiten aktualisiert wird. Hinweis: Sie werden Kenntnisse über den Modulo-Operator%
benötigen
Digitalstoppuhr
Die Digitalstoppuhr soll nach dem Vorbild der verbreiteten Digitalanzeigen gestaltet werden. Hierbei handelt es sich um eine sogenannte Segmentanzeige. Optisch könnte das so aussehen:
Gehen Sie hierzu wie folgt vor:
- Ändern Sie die
Main.kt
so ab, dass anstatt derTextstoppuhr
eineDigitalstoppuhr
an die passende Referenz gebunden wird. - Initialisieren Sie die values der einzelnen Ziffern in der Datei
Digitalstoppuhr.kt
. Wählen Sie geeignete Parameter. - Fügen Sie im
init-Block
die initialisierten Ziffer-Objekte mit Hilfe der MethodeaddChild(CanvasElement)
ein. - Ergänzen Sie die Methode
onEveryFrame()
so, dass die Ziffern-Objekte korrekt aktualisiert werden.
AnalogStoppuhr
Die Analogstoppuhr besitzt die komplexeste Implementierung.
- Ändern Sie die
Main.kt
so ab, dass anstatt derDigitalStoppuhr
eineAnalogStoppuhr
an die passende Referenz gebunden wird. - Initialisieren Sie die values der beiden Zeiger in der Datei
Analogstoppuhr.kt
. Wählen Sie geeignete Parameter, so dass die Zeiger immer in der Mitte des angedeuteten Ziffernblattes angezeigt werden. - Fügen Sie im
init-Block
die initialisierten Zeiger-Objekte mit Hilfe der MethodeaddChild(CanvasElement)
ein. - Ergänzen Sie die Methode
onEveryFrame()
so, dass die Zeiger-Objekte korrekt aktualisiert werden und sich entsprechend der vergangenen Zeit korrekt drehen.
Hinweis: Mit der PropertyrotationDegrees
können Sie den Drehwinkel der Zeiger festlegen.
Erweiterungen
Implementieren Sie folgende Erweiterungen:
Die Stoppuhren funktionieren.