In der letzten Vorlesung hatten wir uns mit dem Thema Vererbung
beschäftigt.
Und vielleicht erinnern Sie sich noch grob, was es damit auf sich
hatte.
Das war ein Mechanismus, um ihre Programme zu strukturieren, um
gewisse Abstraktionen einzuführen.
Und was hat es mit der Vererbung auf sich?
Sie konnten allein vom syntaktischen Konstrukt her, konnten Sie eine
Klasse von einer anderen Klasse ableiten mit diesem
Extentschlüsselwort.
Das heißt, wenn Sie eine Klasse geschrieben haben, konnten Sie
angeben, eine Oberklasse zu dieser Klasse, von der dann Methoden und
Attribute übernommen wurden.
Und wozu macht man das Ganze?
Um eben gemeinsame Aspekte, die vielleicht bei mehreren Klassen
auftauchen, in einer Oberklasse oder in einer Basisklasse
zusammenfassen zu können.
Wir hatten verschiedene Beispiele.
Wir hatten ein Beispiel mit den Tieren, erinnern Sie sich vielleicht
noch dran.
Da hatten wir erstmal die gröbste Klassifizierung, die Lebewesen, dann
Pflanzen, Tiere, dann die Tiere wieder unterteilt in Säugetiere.
Vögel waren ein anderes Beispiel und so weiter, bis wir bei wirklich
speziellen Tieren angekommen sind, wie zum Beispiel Hund und Katze.
Ein anderes Beispiel war die Angestellten in einem Unternehmen, wo wir
auch gesehen haben, dass es eben bestimmte Gemeinsamkeiten da gab, wie
man zum Beispiel die Mitarbeiter und die Manager modellieren kann in
einem Programm und dass da eben Dinge wie zum Beispiel Name oder die
Nummer eines Angestellten eben gemeinsame Dinge waren, die man dann
eben auch in so einer Basisklasse dann nur einmal beschreiben wollte.
Gut, soviel zum Einstieg, dass wir wieder so grob an derselben Stelle
sind und Sie sich vielleicht wieder erinnern, was wir da gemacht
hatten.
Und jetzt schauen wir uns noch ein paar speziellere Themen dazu an.
Also wir hatten letztes Mal auch schon das Überschreiben von Methoden
uns angeschaut.
Also wenn Sie eine Methode in einer abgeleiteten Klasse modifizieren
wollen, dann können Sie das auch tun, indem Sie eben eine Methode
desselben Namens in der abgeleiteten Klasse verwenden.
Im Normalfall schreibt man dann auch das Schlüsselwort add override
noch davor, um dem Compiler anzuzeigen, dass man wirklich
beabsichtigt, eine Methode zu überschreiben.
Wir hatten das Schlüsselwort super.
Und was wir uns jetzt als nächstes anschauen, sind Konstruktoren.
Also wir hatten ja gesehen, dass in einer abgeleiteten Klasse
praktisch alles vererbt wird.
Die Attribute, die Methoden, die vorhanden sind, aber es gibt eine
Ausnahme.
Denn die Konstruktoren werden nicht vererbt.
Und das ist möglicherweise auch, da kann man sich denken, warum das
sinnvoll ist.
Weil ja in der abgeleiteten Klasse zusätzliche Attribute dazukommen,
sodass der Konstruktor der Oberklasse nicht mehr unbedingt Sinn macht.
Also denken Sie vielleicht nochmal zum Beispiel mit den Personen in
einem Unternehmen.
Die Manager, die hatten eine Liste von Angestellten, für die sie
zuständig sind, die in ihrem Team mitarbeiten.
Und diese Liste kann man natürlich nicht in der Basisklasse in einem
Konstruktor anlegen, weil die eben nur in der abgeleiteten Klasse zur
Verfügung steht.
Von daher macht der Konstruktor aus der Basisklasse nur begrenzt Sinn
in der abgeleiteten Klasse.
Also Konstruktoren werden nicht vererbt.
Trotzdem kann man natürlich Konstruktoren auch von der Oberklasse
verwenden wollen.
Weil eben gemeinsame Dinge wie Namen, Mitarbeiternummer oder so, eben
dann doch wieder gleich initialisiert werden.
Und wie man das macht, das sehen wir jetzt.
Und zwar ist es so, dass in einem Konstruktor als erstes immer der
Konstruktor der Oberklasse aufgerufen wird.
Sodass man damit praktisch schon eine Teilinitialisierung der
abgeleiteten Klasse hat.
Und das geschieht entweder automatisch oder indem Sie das explizit
hinschreiben.
Wie genau das unterschieden wird, sehen wir gleich.
Und wenn Sie das explizit hinschreiben, dann ist natürlich die Frage,
unter welchem Namen Sie den Konstruktor ansprechen der Oberklasse.
Und dazu gibt es ein spezielles Schlüsselwort, super.
Und in Klammern hinter diesem Schlüsselwort super geben Sie einfach
die Argumente des Konstruktors der Oberklasse an.
Super muss auch immer die erste Anweisung in einem Konstruktor sein.
Das heißt, Sie können nicht erst einen kleinen Programmcodeschreiben,
der Ihre Attribute der abgeleiteten Klasse initialisiert und dann erst
mit dem Superaufruf die Attribute der Oberklasse initialisieren,
sondern super muss immer das erste sein.
Und wenn Sie nicht selber in Ihrem Konstruktor die super Methode
aufrufen, dann wird ähnlich wie bei dem Konstruktor von der normalen
Klasse, wo ja dis aufgerufen wird, wird hier super ohne Parameter
aufgerufen.
Und so eine super Methode ohne Parameter, die ist in jeder Klasse eben
immer vorhanden.
Genauso wie Sie diesen dis Aufruf ohne Parameter auch in der Klasse
immer vorhanden haben.
Ein gewisses Problem gibt es jetzt noch.
Wir hatten das ja gesehen bei Konstruktoren für eine Klasse, dass man
da auch mehrere angeben kann und dass es im Normalfall sinnvoll ist,
wenn man eben einen Hauptkonstruktor hat und andere abgeleitete
Konstruktoren.
Also denken wir vielleicht wieder an unser Beispiel Vector2D, also ein
Punkt im zweidimensionalen Raum.
Da könnte man einen Konstruktor schreiben mit zwei Attributen, der
eben die x- und y-Koordinate initialisiert.
Und man könnte auch einen nullstelligen Konstruktor haben, der eben
beide Koordinatenwerte einfach auf null setzt.
Und da hatten wir gesehen, dass man eben diesen zweiten Konstruktor
ohne Argumente so programmieren kann, dass man eben diesen anderen
Hauptkonstruktor aufruft, den nimmt es zum Hinschreiben dis und dann
in Klammern eben die Argumente Ihres Hauptkonstruktors.
Und diese dis-Anweisung muss ja auch die erste Anweisung in dem
Konstruktor sein.
Und da entsteht jetzt natürlich hier ein Konflikt an dieser Stelle,
wenn Sie gleichzeitig super als erstes Schlüsselwort in Ihrem
Konstruktor haben wollen und dis als erstes Schlüsselwort.
Und dazu hat eigentlich auch Java keine wirklich gute Lösung.
Da müssen Sie dann eben von Hand drumherum programmieren, indem man
dann eben...
Also super ist normalerweise das, was das Wichtigere ist und statt dis
können Sie dann zum Beispiel eine eigene Methode schreiben, die eben
praktisch eine ähnliche Funktion wie ein Konstruktor übernimmt.
Aber das müssen Sie dann von Hand auflösen.
Okay, also vielleicht nochmal das Wichtige.
Sie haben super, um die Attribute der Oberklasse zu initialisieren und
danach typischerweise initialisieren Sie dann Ihre Attribute der
Unterklasse.
Gut, schauen wir uns das an einem Beispiel an.
Also Dog war ja eine Klasse, die von Mammal, also von Säugetier
abgeleitet war, Säugetier wieder von Animal dann.
Und das heißt, hier könnten Sie zum Beispiel in der Klasse Dog mit
diesem Superaufruf dann die nächsthöhere Klasse initialisieren.
Also das wäre dann eben diese Säugetierklasse, die Mammal-Klasse, wo
Sie den Konstruktor aufrufen und die damit initialisieren.
Dieser Konstruktor wiederum kann einen weiteren Konstruktor in einer
möglicherweise nochmal vorhandenen weiteren Oberklasse aufrufen.
Also falls es in Animal eben nochmal einen Konstruktor gab, wird
dieser auch aufgerufen.
Und das bedeutet dann letztendlich, dass Sie in so einem Konstruktor
dann, wenn Sie eine tiefere Klassenhierarchie, eine verschachtelte
Klassenhierarchie haben, dann eben schrittweise von unten von Ihrer
abgeleiteten Klasse bis nach oben gehen und alle Konstruktoren
aufrufen.
Okay, das ist noch so eine letzte Anmerkung hier, wenn Sie den
Superkonstruktor aufrufen ohne Argumente, dann muss die Oberklasse
entweder einen Konstruktor ohne Parameter besitzen oder der
Defaultkonstruktor wird aufgerufen.
Das heißt, dann dürfen keine weiteren Konstruktoren mehr vorhanden
sein, weil ja sonst der Defaultkonstruktor verschwindet.
Gut, genau, das ist nochmal dieser Punkt, den ich gerade genannt
hatte.
Soviel zu Konstruktoren.
Also der wichtige Punkt, Konstruktoren in abgeleiteten Klassen müssen
Sie selber schreiben, die Initialisierung der Basisklassen machen Sie
mit so einem Superaufruf.
Vielleicht nochmal, wenn wir uns der Frage zu, was wird denn jetzt
alles vererbt, wenn Sie von einer Oberklasse eine Unterklasse
ableiten?
Wir hatten gesagt, ja, im Prinzip wird alles vererbt, Attribute und
Methoden, aber wie verhält sich das mit der Sichtbarkeit?
Also es gab ja noch diese Sichtbarkeitsmodifikatoren Private, Public.
Und was bedeutet denn das, wenn Sie da zum Beispiel jetzt Private
dastehen haben?
Was hat das für eine Bedeutung in einer abgeleiteten Klasse?
Und das schauen wir uns jetzt auch nochmal ein bisschen genauer an.
Also diese Sichtbarkeitsmodifier, und die erweitern sich jetzt noch
etwas im Zusammenhang mit Vererbung von Klassen.
Und es gibt hier ein zusätzliches Schlüsselwort Protected.
Und Sie erinnern sich vielleicht, wir hatten in einer der letzten
Vorlesungen schon mal so eine ähnliche Tapelle, wo Public, Private und
dieser Strich eben kein Modifier schon angegeben waren.
Und jetzt haben wir hier noch zusätzlich auf der linken Seite noch
diesen Protected-Modifikator.
Und mit diesen vier verschiedenen Möglichkeiten, also entweder einen
dieser drei Modifikatoren anzugeben oder gar keinen anzugeben, kann
man schon sehr gut steuern, wo welche Dinge in Ihrem Programm sichtbar
sind und verwendet werden können.
Gehen wir mal der Reihe nach durch.
Fangen wir oben an, die erste Zeile.
Da haben wir den Public-Modifikator und der Name drückt es ja schon
aus, da möchte man eben, dass das öffentlich ist, also von überall
gesehen werden kann.
Und deshalb ist so ein Public-Attribut oder eine Public-Methode
sichtbar, sowohl in der Klasse als auch in dem Paket, wenn Sie ein
Paket verwenden mit dem Package-Schlüsselwort, als auch in
abgeleiteten Klassen, als auch in sonstigen Klassen, die einfach
irgendwo in Ihrem Programm sonst noch auftauchen.
Also Public ist wirklich überall sichtbar.
Wenn Sie Protected verwenden, dann ist das sichtbar in der Klasse, in
der Sie das deklarieren, in dem Paket, mit dem die Klasse gehört und
auch in den Unterklassen, nicht aber in der Welt.
Und das ist ein Unterschied zu Private.
Also wenn Sie Private mal ganz unten schauen, dann sind Private
-Methoden nur in der Klasse selbst sichtbar, aber nicht in
abgeleiteten Klassen.
Also das heißt, wenn Sie eben jetzt von Ihrer Oberklasse in eine
Klasse ableiten, dann sind die Private-Attribute und Methoden nicht
sichtbar in der abgeleiteten Klasse und können dort nicht verwendet
werden.
Und bisher war ja so grob die Strategie, dass man sagt, man verwendet
Public als Schlüsselwort, wenn die Methode was ist, was von außen
gesehen werden soll.
Bei Attributen im Normalfall Private.
Und es ändert sich jetzt ein bisschen, wenn man abgeleitete Klassen
verwendet, dass man da bei Attributen dann nicht mehr zwingend das
Schlüsselwort Private das Beste ist und dass Sie manchmal eben dieses
Protected dann einsetzen müssen.
Und zwar genau dann, wenn Sie eben auf die Attribute auch von
abgeleiteten Klassen zugreifen wollen.
Also das macht man im Normalfall auch nicht so häufig, sondern die
Standardtechnik ist ja, dass man eben Private Attribute hat und
irgendwelche Zugriffsfunktionen, die dann Public sind und über die
können dann auch die abgeleiteten Klassen zugreifen.
Aber in manchen Fällen möchte man eben auch ohne solche
Zugriffsfunktionen, dass die abgeleitete Klasse zugreifen kann.
Und das kann man dann eben über das Protected steuern.
Wenn Sie nichts angeben, dann sehen Sie hier, das ist dann sichtbar in
der Klasse, im Paket.
Und dann bei Unterklassen gibt es noch eine kleine Unterscheidung.
Das ist in Unterklassen sichtbar, wenn Sie im selben Paket sind.
Wenn Sie in anderen Paketen sind, die abgeleiteten Klassen, was auch
geht, dann sind Sie ohne einen Sichtbarkeitsmodifikator dort nicht
sichtbar.
Okay, also Sie haben diese drei Schlüsselwörter Public, Protected,
Private.
Und im Normalfall ändert sich nichts groß an der Strategie, so wie Sie
das bisher verwendet hatten.
Also auch zum Großteil werden Public und Private als Modifikatoren
verwendet.
Und in Ausnahmefällen kann es eben vorkommen, dass Sie auch einer
abgeleiteten Klasse Zugriff auf Attribute oder auch auf Methoden,
Private -Methoden erlauben wollen.
Und das können Sie dann machen, indem Sie dieses Protected
-Schlüsselwort verwenden.
Gut, und das hatten wir eigentlich auch schon gesagt, also diese
Schlüsselwörter zur Sichtbarkeitseinschränkung, die dienen auch
nochmal der Realisierung des Geheimnisprinzips.
Das heißt, Sie wollen möglichst wenig von Ihrer Implementierung nach
außen bekannt geben, sondern nur das, was wirklich notwendig ist,
damit, wenn Sie eben Änderungen an Ihrem Programmcode machen müssen,
weniger Abhängigkeiten zu anderen Programmteilen vorhanden sind.
Also die anderen Programmteile sollen nur die Dinge, die Sie
öffentlich über Ihre Implementierung bekannt geben, wissen.
Also deshalb soll das eben möglichst eingeschränkt sein.
Gut.
Als weiteres gibt es noch ein Schlüsselwort InstanceOf.
Das hatten wir, glaube ich, mal ganz kurz in einem früheren Beispiel
gesehen, aber jetzt nochmal ein bisschen ausführlicher.
Also mit InstanceOf können Sie prüfen, ob der Typ einer Variablen,
also der Laufzeittyp einer bestimmten Klasse angehört.
Also der InstanceOf-Operator prüft, ob ein gegebenes Objekt einen
gegebenen Typ hat oder haben kann, also wie man die Klasseninhalte
berücksichtigt.
Syntax ist relativ einfach.
Wir schreiben hin, Objekt, InstanceOf, Klasse.
Schauen wir uns mal so ein Beispiel an.
Wir könnten zum Beispiel jetzt prüfen wollen für ein Säugetier, ob es
sich bei diesem Säugetier um einen Hund handelt.
Und je nachdem wollen Sie eben ausgeben, ob dieses Säugetier Hund ist
oder nicht.
Und dazu könnte man so eine Methode verwenden, wie sie hier steht.
Also wir haben hier eine Methode isDog, die als Argument eine
Referenzvariable vom Typ Säugetier oder Mammal nimmt.
Und jetzt wollen wir prüfen, ob es sich bei dem Objekt, das hier
übergeben wurde, also es kann ja entweder ein Säugetierobjekt selber
sein oder ein abgeleitetes Objekt, also könnte ein Hund sein oder eine
Katze oder was wir eben sonst noch an zusätzlichen abgeleiteten
Klassen von Säugetieren hatten.
Und um jetzt eben zu prüfen, ob diese Variable M zur Laufzeit den Typ
Hund hat, also den dynamischen Typ Hund, kann man eben hinschreiben M,
InstanceOf, Dog.
Und dann wird eben geprüft, ist die Variable von dem Typ Hund oder
eben auch eine abgeleitete Klasse von Typ Hund.
Also wenn es da noch weitere abgeleitete Klassen gibt, dann werden die
hier auch miterfasst.
Was ich auch noch dazu sagen sollte, ist, dass man im Normalfall sehr
sparsam umgehen sollte mit diesem InstanceOf-Konstrukt.
Und Sie erinnern sich vielleicht auch noch daran, dass wir in den
letzten Vorlesungen ein Beispiel hatten, wie man früher diese
Unterscheidung in anderen Programmiersprachen programmiert hatte.
Wir hatten dieses Modula 2 Beispiel, wo wir gesehen haben, da ist eine
große Fallunterscheidung eben verwendet worden, um festzustellen,
welche Methode eben für ein bestimmtes Übergebnisargument aufgerufen
werden muss.
Und solche Fallunterscheidungen, die bekommt man dann eben hier mit so
einem InstanceOf-Operator auch wieder rein.
Und wir hatten ja auch gesehen, wenn Sie eben solche
Fallunterscheidungen machen, wird Ihr Programm weniger wertbar.
Also wenn Sie hier in Ihrem Programmcode zum Beispiel der Reihe nach
prüfen, erstmal handelt es sich bei Ihrer Variable um einen Hund, dann
um eine Katze und eben so eine ganze Kaskade haben, wo Sie einzelne
Fälle durchprobieren.
Wenn Sie dann Änderungen machen und zum Beispiel noch ein weiteres
Säugetier dazufügen, müssen Sie an dieser Stelle eben auch immer Ihren
Code wieder mit anpassen und nochmal eben diesen zusätzlichen Fall für
die weitere Klasse dazufügen.
Das macht die Programme eben weniger wertbar, weil es eben
verschiedene Stellen gibt, an die man eventuell gar nicht gedacht hat,
wo noch Änderungen notwendig sind, wenn man Erweiterungen vornimmt.
Und deshalb sollte man mit diesem InstanceOf-Operator sehr sparsam
umgehen.
Was wäre denn eine Möglichkeit, wie man das vermeiden könnte?
Also wenn Sie sowas ähnliches wie das, was hier dran steht, eine IsDoc
-Funktion haben wollen, also sagen wir mal vielleicht sogar nicht als
statische Methode wie hier, sondern einfach als eine Objektmethode,
die eben die entsprechende Funktion hat, wie würden Sie das
realisieren ohne InstanceOf?
Haben Sie da eine Idee, wie man das machen könnte?
Also nehmen wir an, wir haben auch wieder eben diese drei Klassen.
Wir haben die Klasse Mammal, Säugetier oder von abgeleitet zwei
Klassen Dog und Cat zum Beispiel.
Und wenn man jetzt da so eine Methode IsDoc in allen drei Klassen
implementieren würde, ohne InstanceOf, dann könnte man das eben wieder
mit diesem Überschreiben von Methoden machen.
Man würde also in der Basisklasse einfach eine Methode, also in der
Mammal -Klasse eine Methode implementieren, IsDoc, die Force
zurückgibt.
In der Klasse Dog würde man eine implementieren, die eben hier dieses
MammalIsDoc ausgibt.
Und in anderen Klassen, wie Katze, würde man eben dann diesen zweiten
Fall, dass es sich nicht um einen bestimmten Hund handelt,
programmieren.
Also man könnte es auch einfach mit einer Methode, die man in jeder
Klasse implementiert, machen.
Und das ist im Normalfall die zu bevorzugende Weise, wie man sowas
implementiert.
Weil dann eben lokal, wenn jemand eine neue Klasse schreibt, nur in
dieser neu zugenommenen lokalen Klasse so eine Funktion implementiert
werden muss.
Und die anderen Dinge, die schon vorhanden sind, die brauchen gar
keine Änderungen.
Okay, also wichtige Dinge.
Mit InstanceOf können Sie prüfen, ob eine Variable den Laufzeittyp
einer bestimmten Klasse hat.
Und das funktioniert eben mit diesem Schlüsselwort InstanceOf.
Also schauen wir uns noch ein Beispiel dazu an.
Wenn Sie sowas aufrufen wie IsDocNewCat, dann wird eben ausgegeben,
dass es sich dabei nicht um einen Hund handelt.
Wenn Sie sowas aufrufen, IsDocNewDog, dann wird natürlich ausgegeben,
es handelt sich um einen Hund.
Was passiert, wenn Sie aufrufen mit der Basisklasse?
Dann ist es auch der zweite Fall.
Also dieses Mammal ist auch nicht vom Typ Dog und deshalb wird auch
IsNotADog ausgegeben.
Vielleicht noch ein Spezialfall, den man vielleicht vergessen kann.
Was passiert, wenn Sie diese Methode mit Null aufrufen?
Also wenn Sie hier bei dem InstanceOf-Operator als erstes Argument die
Nullreferenz haben, auch dann liefert es immer Force zurück.
Also die Nullreferenz, die gehört zu keiner Klasse.
Gut, schauen wir uns noch Typumwandlungen an.
Wir hatten Typumwandlungen schon gesehen im Zusammenhang mit den Basis
-Datentypen oder wie Integer, Boolean.
Aber für Referenzdatentypen hatten wir uns das nicht angeschaut.
Auch hier ist es so, dass wir eben Umwandlungen von Typen vornehmen
können.
Manche gehen automatisch, andere wiederum nicht.
Und hier ist jetzt ein Beispiel von etwas, was automatisch
funktionieren würde.
Also wenn wir sicher sind, dass wir es zum Beispiel mit einer Katze zu
tun haben, dann können wir auch ein Objekt, auch wenn das vom
statischen Programmtext her nicht danach aussieht, können wir das in
den entsprechenden Typ umwandeln.
Und dann kann eben auch auf diesem Objekt so eine Miau-Methode zum
Beispiel aufgerufen werden.
Und hier sehen wir das.
Also das ist etwas, was sehr häufig im Zusammenhang mit diesem
InstanceOf -Operator verwendet wird.
Also wir haben auch wieder so eine Methode, ähnlich wie diese IsDog,
die jetzt eben so eine Katze miauen lassen soll.
Und was machen wir dazu?
Also das Argument dieser Methode ist einfach nur ein beliebiges
Säugetier.
Also als erstes prüfen wir, ob es sich um eine Instanz einer Katze
handelt.
Und wenn das der Fall ist, dann wandeln wir diese Variable um in eine
vom Typ Katze.
Das heißt, wir wandeln den statischen Typ um.
Dynamisch handelt es sich zwar immer um eine Katze, aber um eben diese
Miau -Methode darauf aufzurufen zu können, müssen wir hier nochmal
explizit sagen, dass es sich um eine Katze handelt.
Das heißt, wir wandeln den Typ um in eine Katze und können dann danach
diese Miau-Methode aufrufen.
Wenn Sie diesen Cast weglassen würden, dann wäre es so, dass eben
diese Miau-Methode nicht auf allen Objekten definiert wäre und dass es
dann unter Umständen zu einem Fehler kommen würde an dieser Stelle.
Schauen wir uns das nächste noch an.
Also wenn wir dann weiterlaufen im Programmcode, hier versucht man
dann wieder diese Variable in einen Hund umzuwandeln und das eben auch
in dem Fall, dass es sich um eine Katze handelt.
Das heißt, an der Stelle müsste dann irgendwas schief gehen.
Also wir wissen, die Variable M hat den Laufzeittyp Katze.
Wir versuchen sie in einen Hund umzuwandeln und das kann nicht gehen,
weil es eben kein Hund an dieser Stelle ist.
Und das heißt, an dieser Stelle würde man dann zur Laufzeit eine
Fehlermeldung bekommen, dass diese Typumwandlung nicht möglich war.
Also ein Typecast ändert den statischen Typ eines Objekts, um eben
bestimmte Methoden aufrufen zu können, die nicht in der Basisklasse
definiert sind.
Also Miau ist nicht in der Basisklasse definiert, deshalb können Sie
die auch nicht aufrufen.
Um sie aufrufen zu können, müssen Sie eben das erstmal umwandeln in
den passenden Typ.
Also hier nochmal so ein bisschen plakativ.
Katze ist eine Katze, sie kann aber auch als Säugetier behandelt
werden, nicht jedoch als Hund.
Und die Syntax von einem Typecast ist auch, wie wir es bisher schon
gesehen hatten, also wir schreiben den neuen Typ, den wir der Variable
geben wollen in Klammern, vor die Variable, also wie oben eben für die
Katze.
Gut, auch hier gibt es wieder ähnlich wie bei den primitiven
Datentypen beide Richtungen.
Da hatten wir ja die Narrowing und die vergrößerten
Typkonvertierungen.
Eine Richtung ging automatisch, die andere nicht.
Auch hier ist es wieder genauso.
Und zwar sind hier die Begriffe Upcast und Downcast gebräuchlich.
Upcast bezeichnet die Umwandlung auf eine Superklasse.
Also auch wieder grafisch, so wie wir die Klassen immer gezeichnet
hatten, dass die abgegleitenden Klassen unten sind.
Und Upcast bedeutet eben, wir gehen praktisch eine Stufe höher in dem
Klassendiagramm.
Also Typumwandlung auf die Superklasse.
Downcast wäre dann die Umwandlung auf eine unterere Ebene.
Und die Umwandlung in eine Superklasse ist harmlos, weil das praktisch
auch die Superklasse wieder der größere Typ ist, in dem mehr Werte
möglich sind.
Also das können wir immer machen.
Und dieser Upcast wird auch implizit durchgeführt.
Sie müssen den also nicht direkt hinschreiben, sondern es passiert
automatisch.
Downcast ist das kritischere.
Also wenn Sie, wie im letzten Beispiel gesehen, ein Säugetier, von
einer Säugetiervariable in eine Katzenvariable umwandeln, dann
schränken Sie ja den Bereich ein, über den die Variable sich bewegen
kann.
Und das wäre dann ein Downcast.
Und solche Dinge, die gehen eben nicht automatisch und sind auch
bedenklich, weil sie eben felschlagen können.
Hatten wir auch gesehen, also wenn Sie versuchen, eine Variable, die
ein Hundobjekt enthält, statisch in so einen Katzentyp oder
andersherum umzuwandeln, dann kann das eben felschlagen zur Laufzeit.
Also das muss immer explizit angegeben werden.
Und man sollte davor auch immer prüfen, ob der Typ überhaupt passt, ob
diese Umwandlung möglich ist.
Und dazu kann man dann eben dieses InstanceOf wiederverwenden.
Gut, so viel zu den Casts.
Da werden Sie in den Übungen noch ein bisschen mehr dazu sehen, wie
Sie das genau anwenden können.
Eine andere Sache ist noch, wenn wir uns überlegen, wir hatten diese
Klassenhierarchie, zum Beispiel für die Tiere.
Und wenn Sie sowas aufbauen, dann macht jemand anders vielleicht noch
eine zweite Hierarchie, die vielleicht für Computer oder für andere,
oder eben Personal zum Beispiel im Unternehmen.
Und Sie haben dann so eine ganze Ansammlung von verschiedenen
Klassenhierarchien.
Und was sich die Java-Entwickler dann gedacht haben, ist, dass sie das
versuchen, in einer einzigen Hierarchie zusammenzufassen.
Das heißt, es gibt ganz oben in dieser Klassenhierarchie eine
besondere Klasse, die die Oberklasse von allen anderen Klassen ist.
Und diese Klasse heißt in Java Object.
Und das ist insofern auch etwas Besonderes, dass jede Klasse von
Object abgeleitet ist, auch wenn Sie das nicht angeben.
Also wenn Sie einfach nur eine neue Klasse anlegen oder deklarieren,
also wenn Sie hinschreiben Class Dog und dann in geschweiften Klammern
eben schreiben, was die Klasse enthalten soll, dann wird diese Klasse
immer im Prinzip von Object abgeleitet.
Also das ist so, wie wenn Sie da hingeschrieben hätten Extents Object.
Also es gibt diese eine Basisklasse Object.
Okay, also die Klasse Object, und die befindet sich in dem Paket Java
lang, ist implizit Oberklasse jeder anderen Klasse.
Also das ist die Wurzel der Klassenhierarchie, von wo aus alle anderen
Klassen abgeleitet werden.
Gut, das kann man jetzt sagen, ja, das ist ganz nett, wenn man sowas
hat, dass man eben alles schön zusammenfasst in einer
Klassenhierarchie, aber das hat in Java auch noch eine weitere
Funktion.
Und zwar gibt es schon bestimmte Methoden, die in dieser Objektklasse
deklariert sind.
Und da kann man sich jetzt auch vielleicht wieder ein bisschen
wundern, was kann denn Java da an Methoden schon eingebaut haben, wenn
man praktisch noch gar nicht weiß, um was für Objekte es sich da
eigentlich handelt.
Dann schauen wir uns das mal an, was es in dieser Objektklasse an
Methoden gibt.
Also was Java schon einmal implementiert, ist eine Methode, um
Gleichheit von Objektreferenzen zu prüfen.
Also Vergleich zwischen zwei Objekten, also das aktuelle Objekt, was
mit this ausgedrückt ist, mit diesem Objekt, was hier übergeben wird.
Das ist eine Methode, die schon in der Basisklasse vorhanden ist.
Auch, das heißt, die können Sie, die equals-Methode können Sie auf
jedem Objekt immer aufrufen, egal, ob Sie da was implementiert haben
oder nicht.
Also Gleichheit zu prüfen geht auf jedem Java-Objekt.
Genauso mit dieser nächsten Methode, toString, die eben für ein
gegebenes Objekt eine textuelle Darstellung generiert.
Auch die ist immer schon vorhanden bei jedem Java-Objekt, was Sie
haben.
Also das liefert eine String-Repräsentation des aktuellen Objekts.
Wenn Sie da selber nichts implementieren, dann bekommen Sie eine etwas
kryptische Ausgabe.
Was Java dann macht, ist, dass es praktisch die Adresse, an dem das
Objekt bei Ihnen im Speicher abgelegt wird, ausgibt.
Aber das kann man zumindest ein bisschen verwenden, um zu
unterscheiden, ob zwei Objekte gleich sind.
Im Normalfall wird man diese Methode aber überschreiben, wie Sie das
wahrscheinlich auch schon in Übungen gemacht haben.
Dann gibt es noch eine Clone-Methode, mit der eine 1-zu-1-Kopie eines
Objekts erzeugt werden kann.
Und falls Sie eben nicht die relativ einfachen Standard-Methoden
verwenden wollen, die Java Ihnen zur Verfügung stellt, dann sollten
Sie die überschreiben.
Und das werden Sie im Normalfall auch tun, zumindest für die equs
-Methode.
Zum Testen ist es auch manchmal ganz praktisch, wenn man die toString
-Methode überschreibt.
Clone ist etwas, was man unter Umständen auch überschreibt, aber das
wird nicht so häufig verwendet.
Gut, also wichtig hier, es gibt diese eine Basisklasse Object, von dem
alles abgeleitet ist.
Müssen Sie nicht explizit angeben, weil Java das eben automatisch
macht.
Und in dieser Objektklasse gibt es die Methode equs-to-string-clone.
Es gibt noch ein paar mehr, die hier aber jetzt nicht aufgeführt sind.
Und Java hat jetzt auch schon eine ganze Reihe von eingebauten
Klassen, das haben Sie ja auch schon gesehen.
Und die sind jetzt eben alle in dieser Hierarie drin.
Wir schauen uns da mal einen kleinen Auszug daraus an.
Also in diesem praktisch Kernpaket von Java, Java lang, da hat es eben
wichtige Klassen schon vordefiniert.
Also Object, die Basisklasse, die Sie hier oben sehen.
Dann davon abgeleitet habe ich jetzt mal sieben Klassen angegeben.
In Wirklichkeit sind es noch viel mehr, was Java zur Verfügung stellt.
Sie sehen links zwei Klassen, Boolean und Character.
Die erinnern so ein bisschen an die Basisdatentypen, die Sie kennen.
Dann gibt es die Klasse Math, die hatten wir auch schon verwendet.
Also Math hat eben Mathematikfunktionen, wie zum Wurzelziehen, Sinus,
Kosinus.
Dann gibt es hier eine Number-Klasse, eine String-Klasse, die hatten
Sie auch schon verwendet.
String-Builder kam auch in Übungen mal vor.
Und System haben Sie auch verwendet.
Da gibt es zum Beispiel diese System.out.println-Methode in dieser
Klasse.
Also das sind Dinge, die in Java so hier eingeordnet sind.
Schauen wir uns noch ein bisschen weiter an.
Das Number ist noch weiter aufgeteilt.
Da gibt es noch praktisch für alle Basisdatentypen gibt es hier
nochmal spezielle Klassen.
Also byte, double, float, integer, long, short.
Und diese Klassen, die Sie jetzt hier sehen und die grün markiert
sind, die diesen elementaren Datentypen entsprechen, könnten Sie sich
fragen, ja, wozu braucht man die hier nochmal in Form von Klassen?
Also man hat ja praktisch das alles schon als elementare Datentypen
und hier haben wir jetzt nicht die elementaren Datentypen direkt,
sondern praktisch Klassen, die eine ganz ähnliche Funktion wie die
Basisdatentypen haben.
Und diese grünen Klassen hier sind sogenannte wrapper-Klassen, die
verpacken eben ein Element des angegebenen Typs hier.
Also zum Beispiel in der Klasse Boolean steckt genau ein Boolean-Wert
drin.
Oder in der Klasse Integer steckt genau ein Int drin.
Und es gibt dann eben Methoden, mit denen man einen Wert reinschreiben
kann.
Also im Normalfall würde man das mit einem Konstruktor machen.
Also Sie könnten zum Beispiel aufrufen New oder Integer i gleich New
Integer in Klammer 5.
Dann hätten Sie so ein Integer-Objekt, in dem eben die ganze Zahl 5
gespeichert wäre.
Und den Wert bekommen Sie wieder raus, indem Sie eine entsprechende
Methode aufrufen.
Die ist bei jeder Klasse ein bisschen unterschiedlich, aber nach einem
System aufgebaut.
Also Sie können Int-Value zum Beispiel auf der Klasse Integer
aufrufen, um ein Integer wieder zurückzubekommen.
Sie können Long-Value auf der Klasse Long aufrufen, Boolean-Value auf
der Klasse Boolean und bekommen dann wieder den Wert, den Sie da
reingesteckt haben, wieder zurück.
Können Sie sich vorstellen, warum man das macht?
Warum man zusätzlich zu diesen elementaren Datentypen nochmal hier
jetzt so eine Darstellung in Klassenform hat?
Hat da jemand von Ihnen eine Idee, wo man das brauchen könnte?
Also ganz allgemein kann man sagen, ja manchmal ist es eben praktisch,
wenn man die Elementaren Datentype auch als Objekte auffassen kann.
Es gibt ja praktisch wirklich diese Zweiteilung in der Java-Typ
-Hierarchie.
Einerseits die elementaren Datentypen, dann die Objektreferenz
-Datentypen und manchmal ist es eben so, dass man auch die elementaren
Datentypen wie ein Objekt behandelt möchte.
Aber da sehen wir nachher vielleicht noch ein paar Beispiele, wo das
deutlicher wird, wozum man das braucht.
Sie können sich das vielleicht aber nebenher schon mal ein bisschen
auch nochmal überlegen, wozum man diese Wrapper-Klassen hier bräuchte.
Gut, also das ist die Java-Klassen-Hierarchie.
Ich möchte jetzt noch auf eine Methode, die in Objekt definiert ist,
ein bisschen näher eingehen.
Und zwar auf diese Gleichheitsmethode.
Das ist eine Methode, die Sie im Normalfall überschreiben wollen.
Eben weil diese Gleichheitsprüfung etwas ist, was in der
Standardimplementierung nur sehr grob ist.
Also da wird einfach nur die Gleichheit von Referenzen geprüft, aber
nicht wirklich die inhaltliche Gleichheit.
Also deshalb ist das etwas, was Sie häufig überschreiben.
Und wenn man jetzt mal reinschaut, wie diese Methode in der Java
-Dokumentation beschrieben ist, dann sieht man, dass es da eine ganze
Reihe von Bedingungen gibt, die so eine IQOS-Methode erfüllen sollte.
Also Sie können da in die Java-Dokumentation reinschauen.
Da finden Sie übrigens auch die ganzen anderen Klassen beschrieben,
wenn Sie mal auf diese Seite gehen.
Also auf diese Seite docs.oracle.com.java.se7.docs.api.
Da finden Sie die ganzen Klassen nochmal detailliert beschrieben.
Und für die IQOS-Methode in Object geht die Beschreibung so los.
Also diese IQOS-Methode indicates whether some other object is equal
to this one.
Die IQOS-Methode implements an equivalence relation on non-null object
references.
Also der wichtige Begriff hier ist Äquivalenzrelation, was Sie da
implementieren sollten.
Und dann geben Sie auch noch ein bisschen genauer an, was denn jetzt
Äquivalenzrelation bedeutet.
Also wenn Sie hier diese Methode überschreiben wollen mit einer
eigenen IQOS-Methode, müssen Sie dafür sorgen, dass diese Methode eine
Äquivalenzrelation implementiert.
Hatten Sie das in Mathematik eigentlich schon, Äquivalenzrelation?
Oder noch nicht?
Ja, manche ja, andere nein.
Zur Sicherheit hier nochmal, also Äquivalenzrelation bedeutet, was
eben jetzt auch hier in der Dokumentation nochmal ausführlich
beschrieben ist, die Methode muss reflexiv sein.
Das heißt, wenn Sie ein Objekt mit sich selber vergleichen, dann muss
das immer gleich sein.
Also für jede Nicht-Null-Referenz x, wenn Sie x equals x aufrufen,
dann muss 2 auskommen.
Das ist eine vernünftige Eigenschaft von so einer Gleichheitsfunktion,
aber Sie müssen eben sicherstellen, dass Ihre Implementierung das auch
erfüllt.
Äquivalenzrelation bedeutet auch, dass es symmetrisch sein muss.
Das heißt, wenn Sie x equals y aufrufen und y equals x, praktisch die
Argumente vertauschen, also statt x gleich y, y gleich x hinschreiben,
dann muss das auch dieselben Werte zurückliefern.
Transitiv sollte die Relation sein, das heißt, wenn Sie drei Werte
haben, x, y, z, x gleich y, y gleich z, dann muss auch x gleich z
sein.
Also hier nochmal mit dieser Equals-Notation hier, wenn x equals y, y
equals z, wenn das beides wahr ist, dann muss auch x equals z wahr
sein.
Und dann gibt es hier noch einen Begriff, der nicht zu der
mathematischen Äquivalenzrelation dazugehört.
Das ist ein bisschen was Spezielleres in der Programmierung.
Die Relation sollte auch konsistent sein.
Und das bedeutet in dem Zusammenhang eigentlich nur, dass wenn Sie
zweimal hintereinander die Methode aufrufen mit den gleichen
Argumenten, dann soll auch dasselbe Ergebnis rauskommen.
Also es darf nicht passieren, wenn Sie zweimal hintereinander Equals
aufrufen, dass dann unterschiedliche Ergebnisse entstehen.
Und das kommt einem vielleicht jetzt komisch vor aus einer
mathematischen Sicht, aber in Ihren Programmen haben Sie ja auch einen
Zustand immer noch, den Sie auch modifizieren können.
Und da könnte es zum Beispiel dann passieren, dass Sie eben eine
Abhängigkeit vom Zustand haben, sodass die Equals-Methode nicht jedes
Mal dasselbe zurückliefert.
Also einfach implementieren könnte man das, indem man entweder einen
Zähler verwendet, den Sie immer hochzählt oder eine Zufallszahl
generiert.
Oder sonst würde Sie eben nicht immer das Gleiche zurückgeben.
Also Äquivalenzrelation plus Konsistenz und dann noch diese
zusätzliche Bedingung für Nicht-Null-Werte.
Wenn Sie mit Null vergleichen, das sollte immer Force zurückgeben für
ein Objekt.
Also Sie haben ein Nicht-Null-Objekt, vergleichen das mit Null, dann
darf da nicht Wahr rauskommen.
Gut, und dann schreiben Sie noch, dass die eingebaute Equals-Methode
eben einen sehr einfachen Gleichheitsvergleich nur ermöglicht.
Das können Sie sich genau nochmal auf der Webseite anschauen oder
lesen Sie es sich zu Hause nochmal durch.
Okay, also was sollten Sie tun, wenn Sie selber so eine
Gleichheitsmethode implementieren?
Sie sollten sicherstellen, dass diese Bedingungen alle erfüllt sind.
Also Reflexivität, Symmetrie, Transzitativität, Konsistenz müssen in
Ihrer Methode auch gelten.
Und was machen Sie dann?
Sie können praktisch so eine Checkliste durchgehen.
Erstmal ganz banale Dinge, die Methode, die Sie schreiben, die muss
Boolean zurückgeben und ein Parameter vom Typ Objekt nehmen.
Dann Reflexivität, das können Sie einfach testen, indem Sie prüfen, ob
die Methode z.B.
True zurückgibt, wenn Sie für den Parameter Objekt, das aktuelle
Objekt, also Dis einsetzen.
Meistens sieht man das schon am Programmcode, wenn man drauf schaut,
ob das gilt oder nicht.
Symmetrie kann in der Implementierung Dis und Objekt ausgetauscht
werden, ohne dass ein Unterschied dabei entsteht, wenn das Programm
das abarbeitet.
Transzitativität, das ist ein bisschen schwieriger, das so knapp zu
prüfen.
Also es kann z.B.
ein Problem sein, wenn Sie bei Gleitkommazahlen so eine Y-Umgebung
testen, dass das dann nicht mehr transzitiv ist.
Da haben die Gleitkommazahlen manchmal ein bisschen unerwartete
Eigenschaften.
Und Konsistenz heißt im Großen, dass die Methode den Zustand des
Objekts nicht verändern darf, damit eben bei jedem Aufruf immer wieder
das Gleiche rauskommt.
Okay, bleiben wir da nochmal kurz hier.
Ein gewisses Problem ist jetzt, wie prüfen Sie diese Eigenschaften ab?
Also wenn Sie Ihre Methode schreiben, klar, dann können Sie das alles
irgendwie im Kopf behalten.
Aber schön wäre es natürlich, wenn z.B.
der Compiler das einfach Ihnen abnehmen könnte.
Und wenn der solche Bedingungen automatisch prüfen könnte.
Das macht er aber nicht, der Compiler.
Haben Sie eine Idee, warum der Compiler das nicht prüft, solche Dinge?
Also der Compiler kann ja schön syntaktische Dinge prüfen.
Aber eigentlich wäre es doch auch schön, wenn man sagen könnte, ja,
oder man schreibt das jetzt irgendwie hin, dass der Compiler das
verstehen kann.
Also nicht nur in englischer Sprache oder deutscher Sprache, sondern
für den Compiler irgendwie verständlich.
Und dann kann er das mitprüfen.
Was denken Sie, warum macht man das nicht?
Hat jemand eine Idee?
Versuchen Sie einfach mal ein bisschen zu raten, warum das
problematisch sein könnte.
Hat jemand von Ihnen eine Vermutung?
Also sagen wir mal, was kann der Compiler?
Ja, Sie haben eine Idee?
Richtig, genau, das geht in die richtige Richtung.
Also der Kollege hat gerade gesagt, dass es eben zu viele verschiedene
IQS -Methoden gibt und dass die sich auch zu deutlich unterscheiden
können in der Implementierung, als dass das etwas ist, was eben
automatisiert getestet werden kann.
Das stimmt auch, geht in die richtige Richtung.
Aber das ist etwas, was man in manchen Fällen doch gerne hätte.
Also jetzt nicht unbedingt hier bei der IQS-Methode, aber wenn Sie ein
Programm schreiben, das eben eine sicherheitskritische Funktion
übernimmt, also im Extremfall des Steuerzahlens, wenn Kernkraftwerke
zum Beispiel oder wie da die Bremenstäbe hoch- und runtergefahren
werden, dann möchten Sie eben wirklich so ähnliche Eigenschaften wie
hier wirklich sichergestellt haben, dass die eben nicht nur getestet
wurden oder dass ein Programmierer halt mal so eine Checkliste
durchgegangen ist und gesagt hat, ach ja, das könnte schon so grob
hinkommen, was ich da programmiert habe, sondern dass es eben wirklich
sicher geht.
Und es ist deshalb ein aktueller Forschungsbereich, wo man versucht,
solche komplexeren Eigenschaften, die nicht eben so ganz einfach von
einem Compiler überprüft werden können, auch prüfbar zu machen.
Also zum Beispiel, dass in jedem Fall in einem Fehlerzustand die
Bremenstäbe ganz eingefahren werden.
Oder bei einem Fahrzeug denken Sie daran, in der Zwischenzeit gibt es
ja auch immer mehr Elektronik, die da eingreift.
Bei einem Auto, wenn das automatische Bremseingriffe oder
Beschleunigungseingriffe macht, dass die eben nur in bestimmten
Situationen wirklich vorgenommen werden.
Auch diese Eigenschaften, die möchte man wirklich mit Sicherheit eben
geprüft haben, dass die immer gelten.
Da gibt es dann sehr aufwendige Verfahren, die extrem hohe Laufzeiten
haben oder zum Teil manuell durchgeführt werden müssen.
Das geht in die Richtung, die sich automatisches Beweisen nennt.
Es gibt verschiedene Verfahren, was man da machen kann.
Es gibt statische Prüfmethoden, die eben versuchen, mit so einem
mathematischen Formalismus das Programm zu überprüfen, ohne dass sie
das ablaufen lassen müssen, ob das in allen Fällen das Richtige macht.
Es gibt solche interaktiven Beweismethoden.
Aber die Grundlage ist eigentlich immer, dass man das erstmal in ein
mathematisches System übersetzt und dann eben Beweise über ein
Programm machen kann, dass eben ein Programm immer in allen Fällen das
Richtige macht.
Und wie gesagt, das ist etwas, was in der Forschung sehr aktuell ist.
Es gibt einige Programmiersprachen, die das schon erlauben, solche
Prüfbedingungen in einer formalen Weise hinzuschreiben.
Eifel ist so eine Programmiersprache, die kommt aus der Schweiz.
Aus Zürich ist nicht extrem weit verbreitet, aber die erlaubt zum
Beispiel solche Kontrakte, sagt man da häufig auch dazu,
hinzuschreiben, also komplexere Eigenschaften, die Funktionen erfüllen
sollen.
Und die Prüfung, das ist wirklich ein aktueller Forschungsgegenstand,
wie man sowas möglichst automatisiert prüfen kann, dass die Programme
eben auch solche komplexeren Eigenschaften erfüllen.
Okay, das ist ein bisschen das Ausblick gewesen.
Die letzten zwei Folien noch zur Vererbung.
Man kann die Vererbung auch einschränken.
Man kann bei einer Klasse das Schlüsselwort Final verwenden und das
schränkt dann die Vererbbarkeit ein.
Final hatten wir schon gesehen in zwei Zusammenhängen.
Das erste war bei Attributen.
Wir hatten sowas wie Final Double Pi zum Beispiel und damit wurde ein
konstanter Wert für dieses Attribut angezeigt.
Bei Parametern hatten wir das schon gesehen.
Also Sie können bei einem Funktionsparameter Final hinschreiben.
Das bedeutet dann, dass das Objekt nicht verändert werden darf oder
der Parameter nicht verändert werden darf im Methodenrumpf.
Und was jetzt eben noch neu dazukommt bei Vererbung ist, Sie können
eine Klasse als Final deklarieren.
Also Sie können hinschreiben, Final Class Klassenname und das bedeutet
dann, dass keine Ableitungen mehr von dieser Klasse möglich sind.
Ich versuche nochmal zu fragen.
Ich bin ja vorher auch zur Antwort gekommen.
Was vermuten Sie, warum könnte man sowas machen wollen?
Warum möchte man bei einer Klasse anzeigen mit so einem Final
Schlüsselwort, dass man davon nichts weiter ableiten kann?
Das ist nicht so einfach.
Man kann sagen, ja das ist ganz nett, wenn man da sowas dokumentiert
und man möchte nicht, dass andere Leute da irgendwelche Dinge auf der
eigenen Klasse aufbauen.
Aus welchen Gründen auch immer.
Aber die Hauptgründe sind eigentlich Sicherheit und Performance.
Warum Sicherheit?
Warum erhöht es die Sicherheit, wenn Sie so eine Klasse, von der
Klasse nicht weiter etwas ableiten können?
Jetzt schauen wir uns dazu mal zwei Beispiele an, wo das verwendet
wird in Java.
In Java wird Java lang System und Java lang String zum Beispiel als
Final markiert.
Das heißt, von diesen Klassen können Sie keine weiteren Klassen
ableiten.
Und das soll die Sicherheit erhöhen, weil dadurch eben festgestellt
ist, dass wenn Sie zum Beispiel so eine String-Methode aufrufen, dass
auch wirklich nur die eingebaute String-Methode von Java aufgerufen
wird und nicht irgendjemand eben eine Klasse davon abgeleitet hat,
eine Methode überschrieben und dass dann eben statt den eingebauten
String -Methoden irgendeine andere von Hand programmierte Klasse
verwendet wird, die an dieser Stelle aufgerufen wird.
Also man hat einfach mehr Kontrolle darüber, welche Methoden ablaufen.
Deshalb ist das ein gewisser Sicherheitsaspekt.
Was bei Java noch dazu kommt, ist, dass es Java auch ermöglicht, das
ist ein bisschen ein fortgeschrittenes Konzept, Klassen übers Internet
nachzuladen.
Das heißt, es gibt auch die Möglichkeit, Java für den Browser ablaufen
zu lassen und Java kann dann bei Bedarf eben Klassen übers Internet
nachladen.
Und da hat man natürlich dann auch wirklich solche
Sicherheitsbedingungen, dass man Sicherheitsgarantieren möchte und da
kann es auch sinnvoll sein, solche Einschränkungen zu machen.
Als zweiter Grund ist hier noch Performance angegeben.
Das ist auch so, dass der Java-Compiler an dieser Stelle bestimmte
Optimierungen machen kann, wenn er weiß, es gibt keine weitere
abgeleitete Klasse mehr davon.
Und wieso das?
Das spielt in erster Linie eine Rolle bei der Frage, welche Methode
aufgerufen werden muss, wenn Sie eine Methode hinschreiben.
Also ist das die Methode in der Basisklasse oder in der abgeleiteten
Klasse?
Und diese Prüfung eben, ob das jetzt noch eine Methode in der
abgeleiteten Klasse ist oder nicht, die kann hier dann entfallen.
Und dadurch sind dann zum Beispiel Dinge wie Inlining möglich in dem
Compiler, wo eben Funktionen ineinander geschachtelt und noch weiter
optimiert werden können.
Das geht ohne das auch nicht.
Okay, also bei Klassen können wir Final angeben, dann ist keine
weitere Ableitung mehr möglich, keine Unterklassen können gebildet
werden.
Und auch bei Methoden können wir Final hinschreiben, dann sind die
praktisch auch so ein bisschen was wie konstant.
Also auch hier ist dann kein Überschreiben mehr möglich.
Verhalten ist damit fixiert.
Als letztes noch gibt es ein Konzept, das nennt sich abstrakte
Klassen.
Was hat es damit auf sich?
Eine abstrakte Klasse ist im Normalfall eben eine Oberklasse, aber
einfach erstmal eine ganz normale Klasse.
Für die keine Objekte instanziiert werden können.
Also sie können in der Klasse die New-Methode nicht aufrufen.
Und wozu kann man jetzt wieder sowas brauchen?
Auch das denken wir uns jetzt vielleicht, ach das macht ja wenig Sinn,
wenn ich eine Klasse hinschreibe, von der ich da gar keine Objekte
anlegen kann.
Schauen wir uns das vielleicht auch an einem Beispiel mal an.
Wenn Sie sich die Klasse Säugetier anschauen, dann kann das sein, dass
Sie sagen, ach ja, Säugetier wollen wir eigentlich gar keine Objekte
direkt von dieser Klasse haben, sondern wir wollen immer angeben, dass
z.B.
vom Programmierer ein spezielles Säugetier benannt werden muss.
Also dass z.B.
nur Objekte Hund, Katze oder sowas existieren können, aber nicht von
diesem abstrakteren Konzept Säugetier oder Animal als noch weitere
Oberklasse.
Also hier kann es sinnvoll sein, dass man eben auch von solchen
abstrakten Begriffen nicht erlauben möchte, Objekte anzulegen, sondern
nur von den abgeleiteten Klassen.
Und dazu dient dann eben das Schlüsselwort Abstract.
Das heißt, Sie können bei Methoden das Wort Abstracte vorschreiben und
das bedeutet dann, dass diese Methoden an dieser Stelle nicht
implementiert sind, aber trotzdem zu dieser Klasse gehören und der
Klasse bekannt sind.
Und diese Methoden müssen dann in der Unterklasse implementiert
werden.
Ansonsten bleiben die meisten Dinge wie bei normalen Klassen, also es
gibt auch Attribute, Konstruktoren, Methoden.
Und vielleicht nochmal kurz zu dem Beispiel, da sehen wir dann, dass
man vor dieser Klasse das Schlüsselwort Abstract schreibt.
Ansonsten sieht die Klasse ganz normal aus, aber dann gibt es hier
unten, also der einzige Unterschied, man kann eben keine Objekte davon
anlegen, und hier unten gibt es noch diese Abstract Void Speak
Methode.
Und das bedeutet, also in dieser Klasse Säugetier soll so eine Methode
Speak schon mal bekannt sein, aber implementiert wird sie an dieser
Stelle noch nicht.
Also eine abstrakte Methode ist es hier.
Das heißt, jede abgeleitete Klasse muss sich dann überlegen, wie sie
diese Speak Methode implementiert.
Also auch nochmal, wir haben ja generell das Konzept bei Vererbung,
dass wir eben gemeinsame Dinge in der Oberklasse zusammenfassen
wollen.
Und hier kann man eben in dieser Oberklasse auch schon Dinge in der
Schnittstelle angeben, die man sagt, das sind zentrale Sachen, ohne
die aber implementieren zu müssen.
Also ob jetzt Speak sowas Typisches ist für ein Säugetier, sei jetzt
mal dahingestellt, aber da kann man eben sich auch verschiedene
Varianten überlegen, wie das dann implementiert werden soll.
Also zum Beispiel bei einer Katze könnte man sagen, ja, das Speak ist
halt hier dann ein Miau, was die Katze von sich gibt.
Aber letztendlich geht es darum, genau, dass wir eben in der
Oberklasse die Methode erst mal nur als abstrakte Methode angibt, dass
die eben in dem Interface, also in der Schnittstelle nach außen hin,
verfügbar sein soll, aber noch nicht hier implementiert ist, sondern
nur in abgeleiteten Klassen implementiert.
Okay, gut.
Damit sind wir am Ende von diesem Teil über Vererbung.
Und wir kommen jetzt zu einem weiteren Teil, der sich mit ganz
ähnlichen Konzepten eigentlich noch beschäftigt.
Also auch hier geht es wieder um Abstraktion, geht es darum, wie man
eben so die Strukturierung in einem Programm verbessern kann.
Und dazu schauen wir uns zwei Java-Konzepte an, die sich Interfaces
und Generics nennen.
Und Interfaces, das ist etwas ganz Ähnliches wie die abstrakten
Klassen, die wir gerade gesehen hatten.
Aber schauen wir uns das vielleicht nochmal an einem motivierenden
Beispiel an, was das ist.
Und dazu schauen wir uns eine Klasse an, die wir ganz am Anfang schon
hatten.
Also wir schauen uns die Punktklasse oder Vector2D-Klasse an.
Erinnern Sie sich noch dran, die hatten eben diese XY-Koordinaten.
Man könnte sich auch überlegen, dass man es in einer anderen
Koordinatendarstellung diese Punkte angeben möchte.
Also zum Beispiel mit Polarkoordinaten.
Was sind Polarkoordinaten?
Also wir haben hier wieder einen Punkt und wir könnten für den hier
angeben in dem Koordinatensystem den X- und den Y-Wert von diesem
Punkt.
Aber man kann eben auch anders die Position bestimmen, indem man
angibt die Entfernung vom Nullpunkt, also diesen Abstand R und einen
Winkel zur X-Achse, unter dem man den Punkt erreicht.
Das ist auch eine Darstellung, die es in der Physik zum Teil
gebräuchlich, wenn man eben mit symmetrischen Phänomenen zu tun hat,
da wird das ganz gerne verwendet.
Aber das ist eben auch eine weitere Darstellung, wie Sie zum Beispiel
eine Punktklasse implementieren könnten.
Also statt X- und Y-Koordinaten verwenden Sie eben R und den Winkel
Phi zur X-Achse.
Polarkoordinaten.
Und wie sieht dann so eine Klasse aus?
Die könnte man sich dann so vorstellen.
Also so ein Punkt in Polarkoordinaten hat zwei Attribute.
Phi, den Winkel, und R, den Abstand vom Ursprung.
Und dann gibt es eben die Methoden, die wir in ähnlicher Form auch
schon bei unserer Vektor-2D-Klasse gesehen hatten.
Es gibt einen Konstruktor, der so einen Phi- und R-Wert nimmt.
Dann gibt es Methoden, um einen Punkt zu verschieben.
Hier jetzt nicht in Polarkoordinaten angegeben.
Also man kann angeben, wie weit man den in die XY-Richtung verschieben
möchte.
Man kann den um einen bestimmten Winkel.
Rotation um einen angegebenen Punkt und einen Winkel und so weiter.
Aber das heißt, so könnten wir auch einen Punkt darstellen.
Und genau im Folgenden, nur so als Seitenbemerkung, wollen wir die
bisherige Darstellung, die wir hatten mit XY-Koordinaten, damit man
ein bisschen die Gemeinsamkeiten und Unterschiede heraushebt, statt
Vektor -2D jetzt hier Cartesian Point nennen.
Also für die kartesische Darstellung eben mit XY-Koordinaten.
Okay, und jetzt nehmen wir an, wir haben jetzt eben diese beiden
Implementierungen.
Punkte mit Polarkoordinaten, Punkte mit kartesischen Koordinaten.
Und wir wollen jetzt eine Liste von Punkten implementieren.
Und was man dazu eben machen kann, ist, man möchte eben die
Gemeinsamkeiten von diesen beiden Punktklassen irgendwie auch wieder
zusammenfassen.
Man möchte Punkte, egal ob sie in Polarkoordinaten oder in
kartesischen Koordinaten dargestellt sind, immer gleich behandeln
können in der Liste.
Und was man dazu machen kann, ist, dass man eben diese Gemeinsamkeiten
wieder zusammenfasst in einem sogenannten Interface.
Und das nennen wir jetzt hier passenderweise einfach nur Point.
Das heißt, da geht dann die Darstellung verloren, die interessiert uns
an dieser Stelle nicht.
Also werden die Punkte über Polarkoordinaten oder kartesische
Koordinaten implementiert, ist erstmal egal, sondern uns interessiert
nur, dass es sich um einen Punkt handelt.
Und auch nur das ist, was eben nach außen wichtig und sichtbar sein
soll.
Und das ist eben genau das, was ein Interface ermöglicht.
Ein Interface ermöglicht, die Public-Methoden nach außen hin bekannt
zu geben, ohne eine Implementierung vorzugeben.
Also ganz ähnlich auch wie das, was wir mit den abstrakten Klassen
gerade gesehen hätten.
Wenn Sie in so einer abstrakten Klasse eben nur Public-Methoden haben
und keine implementieren, alle als abstrakt angeben, dann ist das
praktisch fast dasselbe wie so ein Interface.
Es gibt kleinere Unterschiede.
Okay, also was ist für so ein Punkt jetzt das, was nach außen sichtbar
sein soll, was eben wichtig für die Benutzung so eines Punktes ist,
ohne dass man jetzt auf die Implementierung eingehen muss.
Das sind eben diese Public-Methoden, die wir gesehen hatten, um einen
Punkt zu verschieben, zu rotieren.
Dann hatten wir noch mehr Dinge wie Distanz zu prüfen, Gleichheit, XY
-Koordinaten zu bekommen oder eben die Distanz vom Ursprung und den
Winkel.
Und das ist praktisch jetzt auch wieder eine abstraktere Ebene, wie
man auf die Punkte zugreifen kann.
Also wir verraten hier jetzt nichts mehr oder nehmen nichts mehr an
über die Implementierung, sondern wir haben nur eine bestimmte
Schnittstelle, die wir voraussetzen, wie wir mit so einem Punkt
arbeiten wollen.
Aber keine Implementierungsdetails.
Und Java ermöglicht das eben mit diesem Interface-Konzept, dass man
wirklich nur so eine Schnittstelle, also eine Bedienungsmöglichkeit
für eine Klasse bekannt gibt, aber nichts über die Details.
Und so ein Interface könnte man dann zum Beispiel in einer Listen
-Implementierung verwenden, dass wir einfach sagen, die Elemente in
dieser Liste sollen jetzt keine Vector2-Objekte mehr sein, keine
Cartesian -Points, keine Polar-Points, sondern einfach irgendetwas,
was eben dieses Punkt-Interface implementiert.
Gut.
Also wir haben gesagt, wir haben dieses Interface von der einen Seite
und irgendwie müssen wir jetzt noch den Zusammenhang herstellen zu den
Klassen, die das jetzt implementieren.
Also Punkt als Interface, als abstrakte Angabe, wie gehe ich mit
solchen Punkten um, was kann ich damit machen?
Und jetzt muss eben eine konkrete Implementierung irgendwie noch mit
so einem Interface verknüpft werden können.
Und dazu wollen wir jetzt hier angeben, dass diese Cartesian-Point und
Polar -Point-Klasse, dass die das Interface-Point implementieren.
Und wie machen wir das?
Man schreibt das praktisch von direkt hin mit diesem Implement
-Schlüsselwort.
Also man kann schreiben, Class Cartesian-Point implements Point, dann
stehen hier die Dinge drin, die die Implementierung ausmachen.
Eben diese privaten Attribute, Konstruktor plus die Methoden, um eben
damit arbeiten zu können.
Genauso für diese Polar-Koordinaten-Darstellung.
Und diese Klassen jetzt, wenn die so ein Interface implementieren,
dann bedeutet das, dass sämtliche Methoden, die im Interface
deklariert sind, von den Klassen eben auch zur Verfügung gestellt
werden.
Also wenn da eine Shift-Methode im Interface vorkommt, dann müssen die
Klassen, die das implementieren, auch diese Shift-Methode irgendwie
realisieren.
Wenn wir eine Rotate-Methode haben im Interface und eine Klasse soll
das implementieren, muss auch diese Methode eben implementiert sein.
Also alle Methoden, die im Interface deklariert sind, müssen in einer
implementierenden Klasse verfügbar sein und sogar als Public, dass man
eben wirklich auf diese Methoden drauf zugreifen kann.
Das ist eher noch so eine theoretische Seitenbemerkung.
Diese Klassen Cartesian Point und Polar Point sind auch Subtypen von
Pointen.
Also das ist so etwas wie eine abgekleidete Klasse, aber eben keine
Klasse, sondern in diesem allgemeineren Zusammenhang, wo man eben auch
Interfaces hat, wo wir das Subtypen nennen.
Praktisch das Analoge zur Ableitung, zur Subklasse, aber eben
erweitert auf Interfaces.
Okay, also was ist ein Interface nochmal?
Das ist eine Sammlung von Methodensignaturen, gibt aber keine
Implementierung vor.
Es legt die Methodennamen und Parametertypen fest, keine Annahmen über
die Implementierung.
Also es gibt wirklich nur die nach außen hin sichtbare Schnittstelle
an.
Es kann selbst zur Typisierung verwendet werden.
Das heißt, man kann das praktisch wie eine Klasse verwenden an anderen
Stellen.
Also so wie wir in unserer Liste von Vector2D Objekten eben Vector2D
als Klasse verwendet haben, kann man da genauso auch ein Interface wie
eine Klasse verwenden.
Und ein Interface muss implementiert werden oder kann implementiert
werden.
Also wenn man es wirklich verwenden möchte, muss es implementiert
sein.
Und das geschieht dadurch, indem man für jede Methode aus dem
Interface eine konkrete Implementierung angibt.
Und was jetzt auch noch interessant ist, auch wieder die Analogie zu
Klassen, auch Interfaces können vererbt werden.
Also das heißt, wenn Sie ein Interface haben, das eine Methode foo
enthält, das Public, das müsste hier gar nicht drinstehen, das ist
automatisch immer so, aber Sie können das dann eben auch erweitern,
auch wieder mit dem Extentschlüsselwort wie bei Klassen und damit eben
zusätzlich zu den bestehenden Methoden in Ihrem Basisinterface noch
weitere Methoden hinzufügen.
Also innerhalb von dem Interface j müssten Sie deshalb sowohl die
Methode bar implementieren, als auch, wenn Sie ein Objekt haben, das
dieses Interface implementieren soll, dann braucht es eine Methode bar
und eine Methode foo.
Noch mal ein bisschen zusammengefasst, wie sehen solche Interfaces
aus, wie geben Sie das an in Java?
Ganz ähnlich wie Klassendeklarationen, nur statt Glas schreiben Sie
Interface nun davor, dann den Interface-Name, dann in geschweiften
Klammern, wir haben jetzt gesehen, Methoden angeben oder auch
Konstanten, also Sie könnten da auch eine konstante Pi deklarieren und
für die Implementierung ist es so, dass Sie eben das Schlüsselwort
Implements verwenden, also Sie schreiben Glas Klassen-Name und dann
nicht wie bei Ableitung Extents, sondern Implements hier hin und
Implements bedeutet dann eben, dass diese Klasse Implementierungen für
sämtliche Methoden des Interfaces bereitstellt.
Und die Klasse kann auch mehrere Interfaces gleichzeitig
implementieren, also wenn Sie in einem Interface zwei Methoden, im
anderen drei andere haben und Ihre Klasse beides implementiert, kann
das auch angeben werden.
Die Namen werden dann mit Komas getrennt und das ist auch ein
Unterschied zu dem Schlüsselwort Extents, weil bei der Vererbung kann
man immer nur eine Klasse angeben, von der geerbt wird, bei Implements
können Sie mehrere Interfaces angeben.
Gut, also wenn man jetzt so etwas hat, dass man mehrere Interfaces
implementiert und die jetzt zufälligerweise dieselben Methoden
enthalten, also wir haben ein Interface I, ein Interface J und beide
enthalten eine Methode Foo mit, sagen wir Void, oder gar keinen
Parametern, also Void als Rückgabetyp, dann müssen Sie die nur einmal
implementieren.
Es geht ja auch gar nicht groß anders, aber es gibt keinen Fehler,
sondern wenn Sie einmal die implementieren, steht sie in beiden
Interfaces zur Verfügung.
Aber das ist eigentlich auch etwas, was relativ selten auftritt.
Und das hat man auch schon oft genug gesagt, in der
Implementierungsklasse muss jede Methode aus dem Interface
implementiert sein.
Gut, wie verwenden wir das?
Nehmen wir ein Beispiel wieder her, wir nehmen an, wir haben die
Methode Foo und jetzt schauen wir uns unser Point Interface an.
Also diese Foo-Funktion oder Foo-Methode, die kann als Parameter eben
jetzt nicht nur eine Klasse als Typ haben für einen Parameter, sondern
da können auch Interfaces direkt angegeben werden.
Deshalb Interfaces sind eben wirklich auch Typen in Java.
Also Sie geben hier einfach ein Interface an und können dann innerhalb
von der Methode eben auf die Methoden im Interface zugreifen.
Und unser Point Interface hat ja eine Shift-Methode, also können wir
auf dieses Objekt, was irgendwie das Interface Point implementiert,
die Shift-Methode aufrufen.
Wir wissen an der Stelle jetzt praktisch überhaupt nichts, wie die
Implementierung aussieht, sondern wir wissen einfach nur, wir kriegen
da irgendwas, was eben dieses Point Interface erfüllt, die
entsprechende Methoden zur Verfügung stellt und die können wir dann in
der Methode verwenden.
Also was passiert, wenn wir so etwas aufrufen?
Wenn wir einen Punkt Q in Polarkoordinaten haben und da die Funktion
Foo aufrufen und dann uns anschauen bei dem zweiten Punkt R in
karthesischen Koordinaten, wenn wir die Funktion Foo aufrufen.
Also erstmal, was man sieht oben in der Methode, hat P den karthischen
Typ Point, was eben so ein Interface-Typ ist und beim Aufruf von
dieser Shift-Methode wird von Java auch wieder ermittelt, welches
Objekt denn genau in dieser Variable drinsteckt.
Das ist ähnlich wie bei Ableitung oder beim Überraten von Methoden.
Also Java prüft an dieser Stelle, was ist der aktuelle Laufzeittyp des
Objekts und ruft dann die passende Methode auf diesem Objekt auf.
Also im ersten Fall, wenn unser Punkt in Polarkoordinaten übergeben
wird, dann ist es eben so, dass oben dieses P in der Methode Foo dann
als ein Objekt vom Typ PolarPoint erkannt wird und eben die
entsprechende Shift-Methode auch darauf aufgerufen werden kann.
Jetzt bin ich ein bisschen zu weit zurück.
Also bei dem ersten Aufruf von Foo wird die Shift-Methode aus
PolarPoint aufgerufen, bei der zweiten die Cartesian-Point-Methode.
Gut, schauen wir uns nochmal das Beispiel der Liste an.
Also wenn wir einen Algorithmus haben, der eben auf einer Liste von
Punkten arbeitet, dann kann man da auch wieder diese Listen als
Interface verwenden, um nicht zu viel über die Implementierung der
Liste sagen zu müssen.
Also was hatte unsere Liste?
Unsere Liste hatte Methoden wie zum Beispiel AddFirst, AddLast oder
GetFirst und GetLast, um Elemente am Beginn oder am Ende anzufügen und
was man dann eben auch machen kann, ist, dass man die Implementierung
an verschiedenen Stellen erstmal ganz vergisst.
Also ein Benutzer von einer Liste möchte gar nicht unbedingt wissen,
wie ist die Liste jetzt implementiert.
Haben wir da eine doppeltverkettete Liste, eine einfachverkettete
Liste und eine Darstellung über Arrays oder sonst andere
Darstellungen, sondern nach außen hin vom Benutzer kann es nur wichtig
sein, was stellt mir denn diese Liste an Methoden zur Verfügung.
Und das kann man hier zum Beispiel verwenden, indem man eben so ein
Interface angibt, das eben genau diese vier Methoden und eventuell
auch weitere angibt.
Also Abstraktion von der Implementierung der Liste.
Durch so ein Interface müssen wir nicht mehr unterscheiden, wie die
Liste genau implementiert ist.
Und jetzt könnte man dann zum Beispiel eine Implementierung von dieser
Liste angeben, also unsere doubly linked Point List.
Das wäre dann eben so eine doppeltverkettete Implementierung von der
Point List und die enthält dann eben genau solche Dinge wie diese
privaten Attribute.
Okay, also Übersicht nochmal.
Interfaces, die erlauben die Definition von Typen über die
Schnittstelle.
Also man gibt eine Menge von Methoden vor, die diesen Typ ausmachen.
Die Implementierung selber findet erst in der Klasse statt.
Das ist nur eine abstrakte Beschreibung einer Schnittstelle, die eben
von anderen Klassen implementiert werden kann.
Objekte können mehrere Interfaces implementieren oder auch mehrere
Typen haben.
Also sie können einer Klasse und mehreren Interfaces angehören.
Wir haben ja gesehen, man kann Extents hinschreiben mit einer Klasse,
von der sie abgeleitet ist.
Bei Implements eine ganze Liste von Interfaces, die implementiert
werden.
Interfaces können nicht distanziert werden, also auch keine Erzeugung
mit new.
Gut, soviel zu Interfaces.
Aber vielleicht machen wir uns noch mal kurz Gedanken über die Liste.
Das ist immer noch ein bisschen umständlich mit den Listen.
Wir hatten jetzt zwar gesehen, dass wir immerhin schon mal von der
konkreten Implementierung der Liste absehen können, indem wir ein
Listeninterface generieren, das nach außen ist, das schon mal
einfacher zu verwenden.
Man schreibt einfach nur noch Liste hin, ohne eben konkret angeben zu
müssen, wie die jetzt implementiert ist.
Aber das geht noch besser, oder es gibt eigentlich noch ein
gravierenderes Problem, was wir haben.
Und das führt uns auch schon zu dem nächsten Begriff, und zwar zu den
generischen Klassen.
Okay, also schauen wir uns gerade mal das Listenbeispiel an.
Nehmen wir an, wir haben jetzt drei verschiedene Aufgaben zu erfüllen.
Als erstes sollen wir mal eine Liste für Point-Objekte erstellen.
Das haben wir uns gerade noch mal angeschaut, wie wir das machen
können.
Wir haben es sogar schon ein bisschen besser gemacht als früher, indem
wir eben diese abstrakteren Interface-Point-List verwendet haben,
statt nur eine Klasse wie WLink-Point-List.
Also wir haben ein Interface und die Implementierung.
So weit, so gut.
Aber dann kommt vielleicht als nächstes jemand und sagt, ja, ich
brauche jetzt gar keine Liste von Point-Objekten, sondern ich hätte
gerne eine Liste von Line-Objekten.
Und was machen wir dann?
Dann können wir natürlich das gleiche wieder machen, wie das, was wir
hier schon hatten.
Wir können wieder ein Interface uns überlegen, das heißt dann jetzt
eben Line-List.
Wir implementieren es auf irgendeine Art, vielleicht diesmal eine
Singly -Linked-Line-List.
Aber im Grunde genommen machen wir dieselbe Arbeit nochmal wie beim
ersten Mal, nur dass eben irgendwie der Elementtyp, der in der Liste
drin steckt, anders ist.
Dass wir jetzt eben statt Punkten Listen haben.
Aber ein Großteil von der Listen-Implementierung wird genau gleich
aussehen.
Das kommt mir schon ein bisschen komisch.
Also wir machen genau das gleiche noch ein zweites Mal, mit minimalen
Unterschieden.
Und jetzt kommt nochmal jemand und sagt, ich bräuchte jetzt eine Liste
von einem dritten Typ.
Und irgendwann mal denken wir uns, warum machen wir eigentlich da
immer wieder genau dieselben Sachen oder nur mit minimalen
Modifikationen immer wieder dasselbe.
Da müsste doch irgendwas geben, wie man das besser machen könnte.
Also wie kommen wir da irgendwie zu was, dass wir eben nicht jedes Mal
für jeden anderen Basis-Datentyp immer wieder dasselbe neu
implementieren müssen.
Also das Problem ist, dass beim Entwurf der Liste es eigentlich egal
sein sollte, welchen Typ die enthaltenen Objekte haben.
Wir würden gerne irgend sowas implementieren, wo wir sagen können,
diese Implementierung soll jetzt für ganz beliebige Listen-Elemente
beliebigen Typs möglich sein.
Und das ist das, was eben die Generics ermöglichen.
Man kann den Typ des Listen-Inhalts, also wie sehen die Listen
-Elemente aus, sind das Punkte, Linien, ganze Zahlen, den kann man
eben generisch machen.
Das heißt, man sieht auch hier wieder von den Details ab und lässt
beliebige Typen zu.
Und wie macht man das?
Wie ist das normalerweise realisiert?
Das gibt es auch in verschiedenen Sprachen, dieses Konzept.
Also wie funktioniert das?
Also erst nochmal die Beobachtung.
Der Code von solchen sogenannten Containerdatentypen wie Listen oder
Bäumen oder sowas ist häufig unabhängig vom Typ der Basisdaten.
Also man programmiert eben immer wieder dasselbe.
Also nochmal genauer, wenn wir irgendwas in eine Liste einfügen
wollen, dann macht der Code überhaupt keinen Unterschied, egal was für
ein Elementtyp das ist.
Und deshalb wollen wir eben diesen Basisdatentyp als Parameter
irgendwie in die Datenstruktur reinstecken.
Also der soll nicht mehr explizit genannt werden.
Point ist unser Elementdatentyp oder Line ist unser Elementdatentyp.
Sondern wir wollen da ein Parameter oder eine Variable haben können.
Genau und das ist das, was die Generics ermöglichen.
Mit Generics können auch Typen Parameter sein und nicht nur Werte.
Also das heißt, wir können in unserem Programmcode praktisch sowas wie
eine Variable hinschreiben für den Typ, der an einer bestimmten Stelle
verwendet wird.
Und was haben wir dann davon?
Einerseits gut, der Compiler kann Typprüfungen durchführen, wenn er
dann explizit ein Typ hat.
Aber das Wichtigste, denke ich, ist diese Verallgemeinerung, dass man
eben den Code nicht immer wieder neu schreiben muss.
Also keine redundanten Implementierungen.
Der Programmcode wird lesbarer, weil eben auch diese ganzen
Duplizierungen vermieden werden.
Und man genau sieht, wo eben jetzt was variabel von einem Typ abhängt
und wo nicht.
Genau.
Und dieser erste Punkt auch noch, also der Compiler kann Typprüfungen
durchführen.
Das ist eher was, was im Zusammenhang mit früheren Versionen von Java
relevant ist, wo das ohne Generics realisiert wurde.
Okay, dann schauen wir uns mal an, wie das in Java implementiert ist.
Also wie machen wir das?
Polymorphe Typen in Java.
Die werden so hingeschrieben, dass man bei einer Klasse in diesen
größeren und kleineren Zeichen oder in den eckigen Klammern einen
Typparameter hinschreiben kann.
Und genau so geht es auch bei Interfaces.
Also Sie können auch bei Interfaces so einen Parametertyp mit angeben.
Und wie verwenden Sie das?
Auch in der Verwendung genau gleich eigentlich.
Sie haben Ihren Klassennamen zum Beispiel oder Ihren Interface-Namen
und geben dann auch wieder in den eckigen Klammern den Basistyp an.
Also wie könnte es aussehen für eine Klassendeklaration?
Sagen wir, wir haben jetzt so eine doppeltverketete Liste.
Dann könnte man so eine Klasse implementieren.
DoubleLink-List, wo eben jetzt der Elementdatentyp variabel ist und
wieder dann eben genannt werden soll.
Also der Parameter für diesen Elementdatentyp nennt sich eben Data
hier in dem Beispiel.
Und verwenden können wir das dann so?
Man würde hinschreiben DoubleLink-List.
Und jetzt eben einen konkreten Typ muss man jetzt einsetzen für diesen
Parameter.
Also Point wäre das, was wir jetzt hier einsetzen wollen bei der
Verwendung.
Und da legen wir dann eben ein neues Objekt an, wo auch für die
Typvariable eben ein konkreter Typ eingesetzt werden muss.
Also schauen wir uns das nochmal an einem Beispiel an.
Und das ist jetzt unser Programmcode, wie wir ihn früher hatten.
Also die doppeltverketete Liste für Vector2D-Objekte.
Also das war jetzt noch so etwas Spezielles, wo eben diese Liste nur
auf Vector2D-Objekte speichern konnte und es überall im Programmcode
eben drin steht.
Also wir sehen, wenn wir von oben her durchgehen, in der Listenzelle
taucht eben dieser Typ schon mal auf.
Hier bei dem Listenzellen-Konstruktor taucht er auf.
Genauso dann hier beim eigentlichen Listen-Konstruktor und bei diesen
ganzen Methoden AddFirst, AddLast, die praktisch in das Interface dann
reingehören.
Also und da wollen wir irgendwie dieses Vector2D loswerden und das
eben durch etwas Allgemeineres ersetzen.
Und das geht eigentlich fast so, wie man es vermutet.
Wir hatten schon gesehen, man kann so einen Typ-Parameter verwenden,
eine Typ-Variable, die man eben dann oben in diesen eckigen Klammern
angibt.
Und das sieht dann so aus.
Also unsere Liste verwandelt sich zu so einer generischen Liste.
Und wir sehen hier oben haben wir jetzt diesen Typ-Parameter T
angegeben.
Das heißt, das ist diese Typ-Variable, wo man nachher konkret was
einsetzen muss, wenn man sie verwenden möchte.
Und in der ganzen Implementierung ist es jetzt so, dass überall, wo
davor der konkrete Typ-Vector2D genannt wurde, jetzt eben die Typ
-Variable verwendet wird und eingesetzt werden kann.
Also unsere Listenzellen, die bekommen jetzt auch den Parameter T.
Dann der eigentliche Zelleninhalt ist jetzt nicht mehr vom Typ
-Vector2D, sondern wird einfach mit dieser Typ-Variable hier
bezeichnet.
Wir schreiben einfach wie eine ganz normale Klasse oder wie einen
normalen Typ die Variable hier hin.
Und das ist an allen anderen Stellen genauso.
Also auch in dem Konstruktor hier, hier Vector2D ist jetzt einfach der
Typ -Variable nur noch T, also der Parameter, für den wir nachher was
einsetzen müssen geworden.
Und das geht genauso durch bis zu den Methoden hier unten.
Und das heißt, wir haben damit jetzt eine Implementierung, die eben
für beliebige Typen funktioniert, die wir für das T einsetzen können.
Das heißt, wir haben da auch wieder eine höhere Abstraktion erreicht,
indem wir eben jetzt an der Stelle absehen vom konkreten Elementtyp.
Gut, was wir jetzt auch noch machen können als weitere Abstraktion
ist, das hat man vorher sogar schon gesehen, dass man noch so ein
Listeninterface dazu nimmt.
Und dieses Listeninterface kann man auch wieder generisch machen.
Also man kann auch wieder sagen, ich habe ein Interface von Listen,
die irgendeinen beliebigen Elementtyp haben.
Den gebe ich dann auch wieder hier als so ein Typparameter an und kann
dann eben angeben für die Methoden in meinem Interface, wie die
aussehen.
Also da ein At First nimmt dann eben auch ein Element, das den Typ T
hat, der zu dieser Liste passt.
Also Liste von Elementen vom Typ T, müssen wir eben hier auch dann die
passenden Elemente einfügen, die auch den Typ T haben.
Okay, und was passiert dann mit unserer Aufgabe, die wir vorher
hatten?
Also wenn wir uns nochmal überlegen, da hieß es erstmal eine Liste für
Punktobjekte implementieren.
Mit unserem generischen Listdatentyp ist das jetzt einiges einfacher.
Also was machen wir da?
Wir schreiben jetzt einfach hin List Point, also hier diesen
generischen Datentyp.
Hier vorne haben wir auch gleichzeitig noch das Interface verwendet.
Also wir brauchen jetzt nur noch irgendetwas, was dieses
Listeninterface für Elemente um Typ Point implementiert.
Und unsere Implementierung hatten wir gerade gesehen, das war eben
diese wLinkList.
Jetzt eben für den Typ Parameter eingesetzt, der Typ Point.
Okay, eine Liste für Lineobjekte, wie würde es aussehen?
Ganz analog dazu.
Die Implementierung von wLinkList bleibt gleich, das Interface bleibt
gleich.
Das Einzige, was sich unterscheidet, ist der Typ Parameter, der jetzt
hier eingesetzt wird.
Das heißt, man muss an dieser Stelle gar nichts neu programmieren,
sondern man verwendet einfach diese generische Listenimplementierung
und unser generisches Interface und hat sofort auch die Liste für
Lineobjekte.
Und genauso natürlich, wenn Sie weitere Objekte haben, die Sie in so
eine Liste reinstecken wollten.
Also, man kann jetzt Listen mit beliebigem Typ einfach erstellen, ohne
dass Sie immer wieder neu denselben Programmcode entwickeln müssen und
schreiben müssen.
Wir hatten auch gerade schon gesehen, diese generischen Interfaces an
der einen Stelle, dass wir die Liste von einem beliebigen Typ T
hatten.
Es gibt jetzt noch eine Sache, wo wir noch ein bisschen weiter das
verallgemeinern können.
Und schauen wir uns erstmal an, was wir bisher gemacht hatten.
Also, wir hatten die Klasse doubly linked List T, die schon mal vom
Elementtyp abstrahiert.
Da haben wir den Typ Parameter eben eingesetzt.
Dann hatten wir das Interface List abstrahiert von der Implementierung
der Liste.
Also egal, ob die jetzt eine doubly linked List oder singly linked
List oder sonst was ist.
Wir können immer das Interface Liste verwenden.
Und das kann man jetzt natürlich auch beides noch zusammensetzen.
Das ist nochmal eine kleine weitere Abstraktion.
Also, was machen wir?
Das bleibt eigentlich ganz ähnlich.
Das Listen Interface hatten wir schon gesehen.
Und was wir jetzt eben hier machen, ist, dass wir hier unten bei
unserer konkreten Implementierung wieder mit Typ Parameter eben
angeben, dass das unser Interface implementiert.
Das macht die Struktur noch ein bisschen deutlicher.
Also, wenn ich es gerade sehe, genau das war es, was wir hier schon
hatten.
Also, das, was wir hier so schön hingeschrieben hatten, das geht
eigentlich erst nach dem Schritt, den wir auf der nächsten Folie
sehen.
Also, man kann diesen Datentyp auf der rechten Seite nur dann als
etwas vom Typ List auffassen, wenn das eben auch dieses Interface
implementiert.
Das heißt, das, was wir hier haben, müssen wir davor machen und dann
können wir das so schön verwenden wie auf der letzten Folie.
Gut, was auch geht, ist, dass man mehrere Typ Parameter verwenden
kann.
Meistens nimmt man dann eine Schreibweise auch wieder bei Klassen, wo
der erste Buchstabe groß ist.
Also, wenn wir eine Klasse schreiben wollen, die für Paare von
beliebigen Objekten oder Objekten beliebigen Typs funktionieren, dann
können wir auch hier dazu so eine generische Klasse mit Typ Parametern
verwenden.
Und hier haben wir dann eben zwei Typ Parameter.
Einmal für die erste Komponente von dem Paar und einmal für die zweite
Komponente von dem Paar.
Den ersten Typ wollen wir dann, der ist hier Alpha genannt, der zweite
Beta.
Und dann ist es wieder analog wie im letzten Beispiel.
Also, wir haben statt konkreten Typen geben wir hier eben diese Typ
Variablen an.
Also, die erste Komponente von diesem Paar-Datentyp hat eben den Typ
Alpha, die zweite Komponente Typ Beta.
Dann haben wir einen Konstruktor, wo eben diese Typ Variablen wieder
auftauchen.
Und wir haben Methoden, die uns die erste und zweite Komponente
zurückliefern.
Und das ist eben auch wieder eine schöne Sache, ganz generisch.
Das heißt, diese Paar-Klasse funktioniert für beliebige Typen, die wir
da reinstecken wollen, für Alpha und Beta.
Okay, ein paar Minuten haben wir noch, dann schauen wir uns noch eine
Sache hier an.
Noch was zum Vergleichen von Objekten.
Wir hatten ja uns die Iglus-Methode schon ein bisschen angeschaut
früher.
Was Java auch schon zur Verfügung stellt, was hier jetzt auch noch mal
drauf ist mit einer eigenen Implementierung, ist so ein Comparable
Interface.
Und das zeigt auch noch mal, wie man solche Interfaces verwenden kann
im Zusammenhang mit solchen Typ-Parametern.
Also, wir haben zum Beispiel so eine Aufgabe, dass Objekte sortiert
werden sollen.
Und um sie sortieren zu können, muss man sie vergleichen können der
Größe nach.
Und vergleichen bedeutet hier eben mehr als nur gleich oder ungleich,
sondern man möchte auch eine Ordnung haben.
Das heißt, man möchte für zwei Objekte sagen können, welches ist das
Größere, welches ist das Kleinere.
Und dazu kann man dann eben so eine Compare-Funktion schreiben.
Das ist so etwas wie hier diese Compare-Ton.
Da sehe ich gerade, da wird noch ein E rein hier.
Und diese Compare-Funktion gibt drei verschiedene Werte zurück, je
nachdem, ob die beiden Objekte, die man vergleicht, kleiner, das erste
kleiner, größer oder gleich dem letzten ist.
Und zwar ist es so, dass wenn das erste Objekt kleiner als das zweite
ist, dann wird eine negative ganze Zahl zurückgegeben.
Wenn sie gleich sind, null, wenn das erste Objekt größer ist, eine
positive ganze Zahl.
Und diese Vergleichbarkeit, das kann man jetzt eben auch wieder in so
ein Objekt oder in ein Interface rausziehen, in dem man sagt, ja, ich
möchte über so ein Interface nur angeben können, dass diese Klasse
eben so eine Vergleichsoperation implementiert.
Und um zu sagen, meine Klasse implementiert eine Vergleichsfunktion,
kann ich dann hinschreiben für meine Klasse Implements Interface
Comparable und damit drücken wir dann aus, ja, diese Klasse erlaubt
eben jetzt Vergleiche.
Und an einer anderen Stelle kann dann jemand beliebige
Sortierfunktionen schreiben, die eben allgemein mit allem umgehen
können, was dieses Comparable Interface unterstützt.
Wir wollen zum Beispiel die Studenten über die Matrikelnummer
irgendwie vergleichbar machen.
Dann können wir so etwas hinschreiben wie Student Implements
Comparable Student und das drückt eben aus, dass diese Studentenklasse
eine Compare-to-Funktion enthält und dadurch eben in einer
Sortierroutine, die zum Beispiel verglichen werden können.
Und das könnte man so implementieren, dass man die einfach über die
Matrikelnummer vergleicht.
Wir haben hier die Compare-to-Methode, um das Interface Comparable zu
implementieren.
Und die könnte man dann zum Beispiel so vergleichen, um eben bei
kleinerer Matrikelnummer des zweiten Studenten eine negative Zahl
zurückzugeben, bei gleicher die Null und ansonsten eine positive Zahl.
Also diese Implementierung wird das Interface hier oben erfüllen.
Gut, damit genug zu Generics für heute und zu Interfaces, machen wir
Schluss und nächste Woche dann nochmal die letzte Vorlesung in diesem Jahr.