Guten Tag, meine Damen und Herren, willkommen zur Vorlesung
Programmieren.
Thema heute nochmal Vererbung.
Das hatten wir in der letzten Vorlesung, letzten Montag, bereits
begonnen.
Sie erinnern sich vielleicht noch, worum geht es bei der Vererbung?
Es geht darum, Generalisierung, Spezialisierung von Typen
implementieren zu können.
Wir hatten als einführendes Beispiel Klassen von Mitarbeitern, eine
weitere Klasse Manager, Kunde, wo es eben sehr große Überschneidungen
gab zwischen diesen Klassen und um da eine wiederholte Implementierung
derselben Funktionalität zu vermeiden, dazu dient dann eben dieses
Konzept der Vererbung.
Also Generalisierung, Spezialisierung.
Als Beispiel hatten wir dann noch hier diese Lebewesen, wo man sagen
kann, zum Beispiel, ein Hund ist eine Spezialisierung, ein Spezialfall
von einem Tier.
Also diese Art von Beziehung lässt sich mit Vererbung modellieren.
Zu den Begriffen nochmal, also diese allgemeinere Klasse, oder die
speziellere Klasse, fangen wir mit der an, heißt Unterklasse, die
allgemeinere Klasse Oberklasse.
Und es gibt da noch eine ganze Reihe von anderen Namen für diese Ober-
und Unterklassen.
Also statt Oberklasse kann man auch Basisklasse sagen.
Also auf dieser Basisklasse basieren dann die abgeleiteten Klassen.
Oder Elternklasse ist auch noch ein weiterer Begriff, der verwendet
wird.
Und für die Unterklasse eben der Begriff Abgeleitete Klasse, im
Englischen Derived Class.
Oder dann eben im Zusammenhang mit diesem Begriff Elternklasse wird
dann auch hier der Begriff Kindklasse für die Unterklasse verwendet.
Child Class im Englischen dann.
Und zum Beispiel hatten wir, dass ein Vogel eine Unterklasse, ein
Spezialfall, also von Animal ist.
Living Organism wäre dann zum Beispiel Elternklasse von Animal.
Und Manager wäre eine abgeleitete Klasse von Employee.
Und man könnte dann eben in Manager die Dinge verwenden, die in
Employee bereits vorhanden sind.
Wie wird das in Java verwendet?
In Java haben wir dazu dieses Schlüsselwort Extents.
Also wenn wir eine neue Klasse definieren oder deklarieren, die eine
Unterklasse von einer anderen sein soll, dann schreiben wir eben diese
neue Klasse als abgeleitete Klasse von der Oberklasse mit Hilfe dieses
Extents -Schlüsselwortes.
Also im Beispiel nochmal Animal wäre ja eine Unterklasse von Living
Organism, also würde man dann schreiben Class Animal Extents Living
Organism, um diese Unterklassenbeziehung hier anzuzeigen.
Wie gesagt, Spezialisierung, Wiederverwendung von Codes sind so die
Dinge, warum man das verwendet.
Jetzt kommen wir zu ein paar weiteren Aspekten, die noch mit der
Vererbung eine Rolle spielen.
Die eine Sache nennt sich Überschreiben von Methoden.
Und zwar bei der normalen Vererbung ist es ja so, dass Ihre
abgeleitete Klasse alles erbt von der Elternklasse.
Also Attribute, Methoden, Konstanten, die als Finals beispielsweise
deklariert sind, also die alle tauchen auch in der Unterklasse auf.
Manchmal ist es aber so, dass Sie in der Unterklasse ein bisschen ein
anderes Verhalten dann doch haben wollen von Ihrer Funktionalität, wie
sie eben in der Basisklasse schon definiert ist.
Und dazu dient dann dieses Konzept des Überschreibens von Methoden.
Also was kann damit erreicht werden?
Das Verhalten einer Methode kann in der Unterklasse, in der
abgeleiteten Klasse angepasst werden noch, und dafür eben diese
spezielle Variante dann.
Also zum Beispiel, wenn Sie für ein Hund ein anderes Verhalten haben
wollen als für den allgemeinen Fall Tier, können Sie das machen, indem
Sie eben eine Methode, die normalerweise einfach vererbt worden wäre,
in der Unterklasse nochmal neu schreiben mit einer anderen
Funktionalität.
Und das nennt sich dann Überschreiben einer Methode, weil Sie eben das
Verhalten, was in der Basisklasse vorhanden ist und was Sie auch in
der abgeleiteten Klasse verwenden könnten, also die Methode kann ja
auch in der abgeleiteten Klasse verwendet werden, das können Sie
hiermit eben überschreiben und modifizieren.
Was dazu notwendig ist, der gleiche Methodenname, also in der
abgeleiteten Klasse schreiben Sie eine Methode rein, die genauso heißt
wie in der Tuberklasse, die hat die gleiche Anzahl von Parametern und
Typen, Rückgabetyp muss nicht unbedingt genau gleich sein, sondern
Rückgabetyp darf auch ein Spezialfall des Rückgabetyps der Oberklasse
sein.
Und was in neueren Versionen von Java der Fall ist, dass es noch die
Möglichkeit gibt, hier durch eine sogenannte Annotation anzuzeigen,
dass man hier wirklich beabsichtigt, eine Methode zu überschreiben.
Also das war in früheren Java-Versionen noch nicht vorhanden und es
hat sich herausgestellt, dass es manchmal eine Fehlerquelle ist, dass
man versehentlich Methoden überschreibt.
Also man hat das eigentlich gar nicht vorgesehen, dass man das
Verhalten von der Methode der Basisklasse verändern möchte.
Es gibt eben zufällig einer Methode in seiner selbstgeschriebenen
abgeleiteten Klasse, den selben Namen, der schon vorhanden ist und das
wurde in alten Java-Versionen einfach akzeptiert, ohne dass der
Compiler darauf hingewiesen hat, dass man hier eine Methode
überschreibt.
Und deshalb ist es jetzt so bei neueren Java-Versionen, dass das
Überschreiben vom Compiler als Warnung angezeigt wird, damit der
Programmierer eben einen Hinweis hat, hier wird eine Methode
überschrieben.
Und um diese Warnung wegzubekommen, kann man hier diese Annotation
addOverride verwenden, die anzeigt, dass man eben hier bewusst eine
vorhandene Methode der Basisklasse überschreibt.
Schauen wir uns das mal ein Beispiel an.
Also sagen wir in unserer Basisklasse Animal haben wir eine Methode
escape, die eben so dient, ein Ausbrech- oder Fluchtverhalten von
einem Tier zu modellieren, wie auch immer das das macht.
Und jetzt ist es so, dass wir diese allgemeine Escape-Methode für
Vögel verändern wollen.
Also für Vögel wollen wir hier ein anderes Verhalten haben, der kann
in dem Fall einfach davonfliegen, was vielleicht ein anderes Tier
nicht macht.
Also schreiben wir hier die Methode escape in der abgeleiteten Klasse.
Bird extends Animal, deshalb ist die von Animal abgeleitet.
Schreiben wir die Methode neu und zeigen durch diese Annotation
addOverride nochmal an, dass wir hiermit wirklich eine Methode
überschreiben wollen, die schon in der Basisklasse vorhanden ist.
Wir sehen auch, der Name der Methode ist gleich, die Parameter sind
gleich, es gibt keine Parameter und der Rückgabetyp ist auch gleich,
also ist es ein Subtyp des vorherigen Typs auf jeden Fall auch.
Hier haben wir noch ein zweites Beispiel.
Ein Pinguin ist ein Spezialfall von einem Vogel, deshalb ist das
Pinguin die abgeleitete Klasse von Vogel.
Bei einem Pinguin wäre es jetzt so, dass dieses Standardverhalten, was
hier oben vorhanden ist, nicht passend ist.
Dieses Verhalten in Escape für den Vogel ist nicht passend, weil der
Pinguin eben schlecht wegfliegen kann.
Und deshalb überschreiben wir hier in dieser weiter abgeleiteten
Klasse Pinguin nochmals diese Escape-Methode und der Pinguin kann dann
zum Beispiel ins Wasser springen und wegtauchen.
Also Sie sehen, man kann hier eben die Funktionalität, die in der
übergeordneten Klasse vorhanden ist, die vererbt werden würde, kann
man einfach übernehmen hier.
Was Sie hier auch sehen, hier haben wir jetzt zweimal überschrieben.
Also einmal hatten wir die Klasse Animal, dann wird die erweitert zu
Bird, Bird wird wieder erweitert zu Pinguin.
Das heißt, diese Escape-Methode gibt es dann dreimal hier.
Die Frage ist dann auch natürlich, welche Methode wird dann in jedem
Fall verwendet.
Sie können sich schon ungefähr vorstellen, dass immer die speziellste
Methode verwendet wird.
Also wenn hier eine Escape-Methode vorhanden ist, wird die verwendet.
Ansonsten, wenn die nicht vorhanden wird, nimmt man eben die
allgemeineren Methoden.
Aber wie das genau funktioniert, das schauen wir uns auf einer der
nächsten Folien noch an.
Also hier das Konzept des Überschreibens.
Noch eine kleine Anmerkung, nur Methoden können überschrieben werden,
also keine Attribute oder Konstanten.
Das heißt, sie können nicht Attribute, es ist auch schwierig, ein
anderes Verhalten für Attribute anzugeben.
Aber als Regel werden Methoden überschrieben.
Wie gesagt, wir haben jetzt hier natürlich die Frage, welche Methode
wird hier aufgerufen.
Weil es eben mehrfach diese Escape-Methode gibt, haben wir gesagt, man
nimmt so grob immer die speziellste Methode, also die, die am
passendsten ist.
Wenn es eben für Pinguin, der ein Spezialfall für Vogel ist, etwas
Spezielles gibt, dann wollen wir das verwenden.
Aber wie das im Allgemeinen funktioniert, das schauen wir uns jetzt
an.
Und zwar nennt sich dieses Konzept, wie die passende Methode
ausgewählt wird, die wirklich ausgeführt wird, nennt sich dynamische
Bindung.
Also dynamische Bindung im Vergleich zu statischer Bindung.
Statisch wäre eben etwas, was irgendwie feststeht, zum Beispiel
während der Kompilzeit.
Dynamisch zeigt es schon irgendwie an, da passiert eben irgendwas,
dass da eben anhand von Programmablauf irgendwas berechnet wird und
die Bindung nicht beim Kompilieren zum Beispiel schon feststeht.
Also dynamische Bindung.
Schauen wir uns das hier an einem Beispiel nochmal.
Wir legen ein Objekt der Klasse Pinguin an, P. Dann legen wir ein
Objekt der Klasse Bird an.
Und dann haben wir hier nochmal ein Objekt, Bird B2, dem wir P
zuweisen.
Das geht.
Sie sehen hier, P ist vom Typ Pinguin, also eigentlich eine andere
Klasse wie das, was wir hier zuweisen.
Aber dadurch, dass das Bird hier eine allgemeinere Klasse ist, eine
Oberklasse von Pinguin, können wir eine Zuweisung in diese Richtung
machen.
Also wir können nicht Variablen, die auf ein spezielleres Objekt
verweisen, anderen Variablen zuweisen, die zu einer allgemeineren
Klasse oder einer Oberklasse gehören.
Und die Frage ist jetzt, welche Methoden werden ausgeführt, wenn wir
hier eben die Escape-Methode aufrufen für die verschiedenen Variablen.
Schauen wir uns einfach mal die einzelnen Zeilen der Reihe nach an.
Also wenn wir hier auf dem Objekt P die Escape-Methode aufrufen, dann
ist es noch relativ klar, dieses P ist ein Objekt der Klasse Pinguin.
Das heißt, wir würden jetzt erwarten, dass diese Methode hier unten
aufgerufen wird.
Und das ist auch der Fall.
Also hier wird der Code aus der Klasse Pinguin, das speziellstmögliche
Code, ausgeführt.
Dann dieser zweite Fall hier, wenn wir ein Bird-Objekt anlegen und auf
dem dann die Escape-Methode aufrufen, ist eigentlich auch relativ
klar, welche Methode verwendet werden soll hier.
Es ist kein Pinguin, deshalb kann diese Methode nicht verwendet
werden.
Also Bird, dann erwarten wir, dass diese Methode, also nicht die hier
oben, sondern diese Methode, die hier überschrieben wurde, dass die
aufgeführt wird.
Auch das ist der Fall.
Also hier wird der Code aus der Klasse Bird ausgeführt.
Und dieser dritte Fall ist eigentlich das Interessanteste.
Also was passiert, wenn wir hier bei B2 Escape aufrufen?
Und hier wären jetzt im Prinzip zwei Dinge denkbar.
Entweder, dass er eben hier feststellt, hier dieses B2, zu welcher
Klasse gehört das?
Zur Klasse Bird.
Also wird hier die Escape-Methode von Bird aufgerufen.
Das wäre eine Möglichkeit, was passieren könnte.
Und die andere Möglichkeit wäre, ja dieses B2, was wir hier drin
haben, ist ja eigentlich ein Pinguin gewesen.
Wir hatten das ja mit New Pinguin, ein Pinguin-Objekt angelegt.
Und dann diese Referenz hier eben nur verändert auf Bird.
Also wäre es auch möglich, dass wir, weil wir eben hier ein Pinguin
-Objekt haben, auch mit diesem Pinguin-Objekt eben entsprechend
arbeiten und diese Escape-Methode dann auf dem Pinguin-Objekt
aufrufen.
Das heißt, in der ersten Variante, das wäre diese sogenannte statische
Bindung.
Das heißt, man schaut sich einfach den statischen deklarierten Typ
dieser Variable an, oder dieser Referenzvariable, auf der die Methode
aufgerufen wird.
Der statische Typ wäre hier Bird und würde dann von diesem statischen
Typ die Methode aufrufen.
Und die andere Variante, das ist dann die dynamische Bindung, die eben
nachschaut, wenn hier so eine Referenz steht, was ist denn eigentlich
wirklich das Objekt, das wir angelegt hatten mit New.
Also worum handelt es sich, was da drinsteckt.
Also es kann entweder ein Bird-Objekt sein, aber es kann auch ein
Objekt einer abgeleiteten Klasse von Bird sein, was da wirklich mit
New angelegt wurde.
Und dieses eben zur Laufzeit zu bestimmen, was ist eigentlich hier in
dieser Objektreferenz für ein Objekt wirklich drin.
In dem Fall ist es ein Pinguin.
Und dann darauf basierend diese Methode aufzurufen, das nennt sich
eben dynamische Bindung.
Und dynamische Bindung ist auch das, was Java macht.
Das heißt, in dem Fall würde jetzt der Code aus der Klasse Pinguin
aufgerufen werden.
Also, obwohl hier dieses B2 hier oben einen Typ Bird hat, schaut Java
dann eben nach, mit welchem Typ wurde dieses Objekt, was hier
referenziert wird, wirklich angelegt.
Das war Pinguin, also wird auch die Pinguin-Methode aufgerufen.
Und das ist etwas, was im Normalfall oder im Allgemeinen nur zur
Laufzeit bestimmt werden kann und noch nicht, während Sie Ihren Code
kompilieren.
Das heißt, während Ihr Programm abläuft, muss das herausfinden, was
ist denn die passende Methode, die jetzt aufgerufen werden muss.
Und weil das eben zur Laufzeit berechnet werden muss, nennt sich das
dann eben dynamische Bindung.
Okay, also schauen wir uns das nochmal ein bisschen formaler an.
Wenn wir einen Methodenaufruf M auf einem Objekt OBJ haben, dann wird
erst zur Laufzeit dynamisch die passende Methodendefinition gesucht
und an diese dieser Aufruf gebunden, sagt man da auch.
Und die Entscheidungsgrundlage, an welchen Typ das gebunden wird, ist
der Typ dieser referenzierten Objekte, nicht der Typ, mit der diese
Variable OBJ deklariert wurde.
Also im Beispiel vorher war unser OBJ, unser P2, als Bird deklariert.
Das ist nicht das Entscheidende, sondern entscheidend ist der Typ des
referenzierten Objekts.
Und was eben unser B2 referenziert hat, war ein Pinguin-Objekt, also
diese Objektinstanz war wirklich ein Pinguin, deshalb wird auch diese
Methode aufgerufen.
Also hier nochmal das Beispiel, wir legen einen neuen Pinguin an und
dieses Objekt, was hier eben wirklich angelegt wurde, entscheidet dann
darüber, was eben für die Methode aufgerufen wird.
Also diese Referenz wird hier weitergegeben an das B, aber trotzdem
steckt natürlich, wenn wir bei B nachschauen, was für ein Objekt das
eigentlich ist, immer noch so ein Pinguin-Objekt drin und alle
Attribute zum Beispiel von Pinguin sind auch vorhanden, deshalb kann
eben auch hier diese spezielle Methode aufgerufen werden.
Okay, also eine kleine Randbemerkung, das ist etwas relativ
Spezielles, die Parametertypen, also wenn Sie hier mehrere Escape
-Methoden hätten mit verschiedenen Parametern, die werden schon zur
Kompilierzeit statisch berechnet, aber das ist nur so eine
Randbemerkung, weil das ein relativ spezieller Fall ist, dass Sie
mehrere Methoden mit unterschiedlichen Parametern haben.
Gut, was ist der Vorteil von dieser dynamischen Bindung?
Der Code ist einfacher erweiterbar, das sehen wir gleich noch an einem
Beispiel.
Nachteil ist, dass wir eben bestimmte Dinge erst zur Laufzeit
berechnen können und wenn wir es zur Laufzeit berechnen müssen, dann
bedeutet das eben auch, dass da Rechenzeit verloren geht.
Das heißt, man hat Code, der etwas langsamer läuft, als wenn man so
eine statische Bindung machen würde.
In anderen Programmiersprachen ist es möglich, beides zu machen,
sowohl statische als auch dynamische Bindung, also C++ zum Beispiel.
Das ist eigentlich der Standardfall, der in der Sprache vorgesehen
ist, die statische Bindung.
Wenn man dynamische verwenden möchte, muss man das extra durch ein
Schlüsselwort anzeigen.
Virtual heißt das Schlüsselwort.
Okay, aber in Java eben die Idee, dass man immer dynamische Bindung
verwendet.
Okay, das schauen wir uns jetzt nochmal an einem zweiten Beispiel an,
um nochmal zu zeigen, warum diese dynamische Bindung auch sinnvoll
ist.
Also hier wollen wir zum Beispiel so eine Grafikbibliothek schreiben.
Die hat eben bestimmte grafische Objekte, die wir auf dem Bildschirm
anzeigen wollen.
Zum Beispiel ein Kreis, dann gibt es irgendwie so ein Rechteck.
Und wir haben eben festgestellt, diese ganzen Grafikobjekte, die haben
doch eine gewisse geteilte Funktionalität, haben wir dazu eine
Oberklasse Shape gemacht, also für grafische Formen, die auf dem
Bildschirm ausgegeben werden können.
Und darin gibt es eben Methoden, die allgemein auf alle
unterschiedlichen Formen, die wir zeichnen wollen, auch zutreffen.
Zum Beispiel so eine Flächenberechnungsmethode Area.
Und im allgemeinen Fall, wenn wir hier einfach nur einen ganz
beliebigen Shape haben, wo wir nichts Genaues darüber wissen, wie der
aussieht, könnte es sein, dass wir da eine relativ komplizierte
Berechnungsmethode brauchen, über Integrale vielleicht.
Aber in anderen Fällen geht es natürlich einfacher.
Also jetzt haben wir von diesem allgemeinen Shape-Objekt abgeleitet
einmal die Klasse Circle.
Und hier in dieser Circle-Klasse, das ist jetzt nicht weit
überraschend, da haben wir den Radius gespeichert von diesem Kreis,
den wir auf dem Bildschirm ausgeben wollen.
Dann haben wir einen Konstruktor und dann hier so eine
Flächenberechnungsmethode.
Und die Flächenberechnung wollen wir eben überschreiben hier.
Das heißt, diese allgemeine, relativ aufwendige Methode soll für den
Kreis jetzt überschrieben werden mit einer spezielleren
Flächenberechnungsmethode.
Ein Kreis ist relativ einfach, das ist Pi L², also Pi mal Radius mal
Radius mal Radius rechnen wir hier aus.
Und durch das Override zeigen wir nochmal an, dass wir es
überschreiben.
Und ähnlich hier drüben beim Rechteck, das hat Attribute Breite und
Höhe.
Die sollten eigentlich auch noch private sein, das fällt hier.
Dann haben wir den Konstruktor und dann hier die
Flächenberechnungsmethode fürs Rechteck, die auch sehr einfach ist,
Breite mal Höhe.
Gut, also so könnte man sowas modellieren.
Wo hilft uns jetzt die dynamische Bindung?
Wenn wir jetzt eine weitere Methode programmieren wollen, eventuell in
einer anderen Klasse nochmal, die uns für mehrere solcher grafischen
Formen oder Objekte, die wir auf dem Bildschirm zeigen wollen, die
Gesamtfläche berechnen soll, dann können wir da so eine Methode
schreiben, Total Area, also die soll aufsummieren die Flächen der
einzelnen Objekte, die wir haben.
Und diese Methode bekommt eben so ein Array von Shapes als Eingabe und
soll jetzt eben für diese Shapes, die in dem Array drinstehen, die
Flächen aufsummieren.
Also unsere Gesamtfläche initialisieren wir erstmal auf 0 und dann
haben wir hier die Schleife, die eben über unsere Bäume, die im Array
drinstehen, drüberläuft und jeweils die Fläche der entsprechenden Form
berechnet.
Also hier Shapes E, rufen wir die jeweilige Area-Methode auf und
addieren die dazu.
Und an der Stelle, da würden wir es eben gerne haben, dass je nachdem,
welche Form wir in unserem Array drin haben, die passende Area-Methode
hier aufgerufen wird.
Also für den Kreis sollte diese PR-Quadratberechnung aufgerufen
werden, für ein Rechteck die Breite mal Höhe.
Und genau das passiert eben jetzt hier durch diese dynamische Bindung.
Also wenn wir zwei solche Shapes anlegen, einen Kreis mit Radius 10,
einen Rechteck, einmal 4,5 und wir schreiben jetzt hier in unser Shape
-Array zweimal den Kreis rein, einmal so ein Rechteck und rufen dann
diese Total Area-Methode darauf auf, dann wird eben für jedes Element
im Array dynamisch bestimmt, was die passende Area-Methode ist, die
aufgerufen werden muss.
Also Sie sehen hier, wenn Sie da eben so eine Methode wie hier dieses
Total Area schreiben wollen, die eben erstmal hier über Objekte von
dieser Oberklasse einfach in einem Array drin hält, dann können Sie da
eben durch diese dynamische Bindung immer die speziellere passende
Methode aufrufen.
Was ist der Vorteil, wenn Sie jetzt eine Erweiterung programmieren,
dass Sie noch eine zusätzliche Form in Ihre Grafikbibliothek
aufnehmen, zum Beispiel ein Dreieck, dann schreiben Sie diese Klasse
Dreieck neu, schreiben darin auch eine Area-Methode, aber Sie müssen
diese Total Area-Methode überhaupt nicht mehr anpassen.
Also in dieser Methode, die muss nicht wissen, dass es da jetzt
zusätzlich noch Dreiecke oder andere Formen gibt, sondern dadurch,
dass eben hier dynamisch immer die passende Methode aufgerufen wird,
ist diese Methode hier so allgemein und so stabil, dass sie eben durch
so eine Erweiterung nicht modifiziert werden muss.
Nochmal, welche Methode wird genau ausgeführt, was ist die
Entscheidungsgrundlage?
Zur Laufzeit wird für dieses entsprechende Bezugsobjekt, den Shapes-I,
wird ausgehend von dem Typ, den dieses Objekt zur Laufzeit hat, also
von dem Typ, mit dem es mit new angelegt worden ist, wird geschaut, ob
in dieser Klasse schon die entsprechende Methode vorhanden ist.
Aber das kann ja sein, dass die gar nicht überschrieben wurde.
Und in dem Fall geht man dann eben nach oben in der Klassenhierarchie,
also schaut in den weiter oben liegenden, allgemeineren Klassen nach,
ob da so eine Methode vorhanden ist und geht das so lange durch, bis
man irgendwann mal eine Methode des passenden Namens gefunden hat.
Das heißt, Sie beginnen in der Klasse, in der Ihr Objekt mit new
angelegt wurde, also mit dem Laufzeittyp des Objekts, schauen nach,
gibt es da die Methode, wenn es für den Spezialfall die Methode nicht
gibt, schauen Sie eben schrittweise in allgemeineren Klassen nach, von
denen es eben abgeleitet wurde, ob es da irgendwann mal die Methode
gibt.
Wir gehen in der Klassenhierarchie nach oben und führen, sobald Sie
zum ersten Mal eine Methode finden, also im speziellsten Fall, dann
diese Methode aus.
Das ist eben diese dynamische Bindung nochmal.
Jetzt vielleicht nochmal zum Vergleichen.
In alten Programmiersprachen, also vor der objektorientierten Zeit,
gab es diese Möglichkeit noch nicht, dieses dynamische Binden zu
machen.
Und da musste man dann eben wirklich mit Fallunterscheidung arbeiten.
Also eine Programmiersprache, die davor auch in der Lehre sehr beliebt
war, heißt Modula 2.
Das ist von Pascal abgeleitet, eine noch ältere Programmiersprache,
die von Nikolaus Wirth in der ETH in Zürich entwickelt wurde, genauso
wie dieses Modula 2 auch.
Also da war das so, wenn man da jetzt so eine Methode Area schreiben
wollte, dann müsste man eben hier so eine Fallunterscheidung machen,
die eben, also es gab da so ein Feld TypeID, mit dem man den Typ
rausfinden konnte schon mal, oder man müsste es von Hand
programmieren, müsste man so ein TypeID-Feld dann richtigerweise, und
da muss man eben so eine Fallunterscheidung machen.
Und hier sehen Sie eben auch den Nachteil, wenn Sie jetzt eine neue
Klasse dazu programmieren würden, in Ihrer Formenbibliothek, dann
hätten Sie eben jetzt hier nochmal Ergänzungen jeweils vorzunehmen in
der Fallunterscheidung.
Also bei Einführung eines neuen Shape Types müssen Sie sämtliche
Methoden, die Ihre Objekte verwenden, nochmal drüber schauen, über
diese Fallunterscheidung, die Sie da dann programmiert haben, und die
Fallunterscheidung entsprechend erweitern.
Und bei größeren Programmen können es sehr viele Stellen sein, wo Sie
so eine Erweiterung vornehmen müssen, und man übersieht vielleicht
auch leicht was.
Und das ist dann eben hier der Vorteil von diesem dynamischen Binden,
dass man eben diese Fallunterscheidung nicht selber programmiert,
sondern dass das praktisch hinter den Kulissen abläuft.
Gut, also überschreiben von Methoden hatten wir gesehen, überschreiben
von Attributen haben wir gesagt, das geht nicht, überschreiben von
Attributen ist in Java nicht möglich.
Wenn Sie trotzdem versuchen, so etwas zu machen, das heißt, wenn Sie
in einer abgeleiteten Klasse nochmal ein Attribut mit demselben Namen
verwenden, dann verschattet dieses Attribut das Attribut der
Oberklasse.
Also das sollten Sie nicht machen, so etwas, weil das kein besonders
guter Programmierstil ist, aber schauen wir uns trotzdem mal an einem
kleinen Beispiel an.
Nehmen wir an, wir schauen wieder unsere Tierklassen vom Anfang an.
Wir haben das Säugetier, da gibt es ein Feld, ein Attribut age, und
dann leiten wir davon ab, diese Klasse Hund, und hier versuchen wir
jetzt so etwas wie das Überschreiben von Attributen.
Wir legen hier nochmal ein Attribut mit gleichem Namen an, das auch
wieder age heißen soll, und dann gibt es jeweils die Methoden setAge
und setDocAge hier, um dieses Attribut von außen zu setzen.
Und wenn wir uns jetzt so ein Hundobjekt anlegen, setAge 3 aufrufen,
setDocAge 21,
dann vielleicht nochmal so eine Referenzvariable der Überortengasse
zuweisen und das ausgeben.
Dann ist jetzt hier die Frage, was wird denn jetzt ausgegeben?
Also bei dynamischer Bindung wäre es so, dass eben dieses Speziellere,
also von dem Objekt, mit dem es angelegt wurde, oder von dem
Laufzellobjekt, die entsprechende Methode rausgesucht würde.
Hier ist das jetzt eben nicht so.
Das heißt, wenn hier jetzt docAge ausgegeben wird, dann wird das
statisch entschieden.
Also der statische Typ ist Säugetier, also wird hier diese Methode
aufgerufen.
Oder dieses Attribut hier abgefragt, dieses Age-Attribut.
Und nachdem das zwei Variablen sind, ist das obere hier durch diesen
unteren, das Setzen auf 21 nicht modifiziert worden.
Und deshalb kommt was dann raus?
Das ist ein bisschen ein esoterischer Fall hier.
Das weiß man vielleicht nicht, aber so wie ich es gerade erklärt
hatte, haben Sie eine Vermutung, was rauskommt.
Also die Frage ist ja, 3 oder 21?
Wer ist für 3?
Kann ich mal kurz fragen.
Eine Stimme für 3, ne 2 sogar, wer ist für 21?
Das sind doch einige mehr noch.
Aber das Überraschende ist, dass hier wirklich 3 rauskommt.
Also weil eben dieses dynamische Binden hier bei den Attributen nicht
ausgeführt wird.
SetAge setzt diese Variable hier oben, SetDocAge setzt die Variable
hier unten.
Und hier bei der Ausgabe wird eben nicht dynamisch das Laufzeittyp des
Objekts bestimmt, sondern einfach der Declarete-Typ genommen und dann
die entsprechende Variable Age hier oben ausgegeben.
Aber wie gesagt, das ist ein bisschen ein esoterischer Fall und Sie
sollten das sowieso vermeiden, dass Sie Attribute in einer
abgeleiteten Klasse überschreiben.
Gut, manchmal möchte man dann doch eben auf irgendwelche Attribute
oder Methoden der übergeordneten Klasse zugreifen.
Und dazu gibt es in Java das Schlüsselwort super, mit dem eben die
Superclass, also die Oberclass, angesprochen werden kann.
Damit wird die dynamische Bindung etwas modifiziert oder aufgehoben,
weil man mit super eben anzeigt, soll jetzt eben nicht das
entsprechende Attribut aus der aktuellen Klasse verwendet werden,
sondern eben von der Superklasse.
Also bei Attributen geht das dann zum Beispiel auch, also Sie können
da hier, wenn Sie in diese abgeleitete Klasse vom letzten Beispiel
nochmal denken, also Hund von Zeugetier abgeleitet, Sie hatten dieses
SatDogAge, das setzt dann eben hier in der Klasse Dog das Age und mit
so einer Methode wie zum Beispiel SatMammalAge können Sie dann über
SuperAge auf dieses Attribut der übergeordneten Klasse zugreifen und
das modifizieren.
Aber auch das ist etwas ungewöhnlich, das Super, das wird in einem
anderen Zusammenhang noch verwendet und ist da eigentlich noch
wichtiger, und zwar im Zusammenhang mit Konstruktoren.
Konstruktoren sind ja ein Spezialfall von Methoden und die haben auch
ein bisschen ein anderes Verhalten im Zusammenhang mit Vererbung, und
zwar ist es so, dass Konstruktoren nicht vererbt werden.
Macht im Normalfall auch nicht so viel Sinn, weil eben die
abgeleiteten Klassen in vielen Fällen ja zusätzliche Attribute
einführen und diese Attribute könnten von diesem
Superklassenkonstruktor gar nicht initialisiert werden, weil der
nichts darüber weiß.
Trotzdem ist es so, dass einige der Attribute aus der übergeordneten
Klasse ja auch so initialisiert werden sollen wie im Allgemeinfall.
Und deshalb ist es so, dass in einem Konstruktor wird immer zunächst
ein Konstruktor der Oberklasse aufgerufen.
Das heißt, in Ihrer abgeleiteten Klasse haben Sie vielleicht
zusätzliche Attribute, die müssen Sie selber initialisieren, aber alle
Attribute der Oberklasse, die möchten Sie sehr wahrscheinlich genauso
initialisieren, wie es eben im Allgemeinenfall in dem Konstruktor der
Oberklasse auch funktioniert.
Und deshalb ist es eben in Java so, dass in einem Konstruktor immer
zuerst ein Konstruktor der Oberklasse aufgerufen wird.
Und das geschieht entweder automatisch oder Sie können das auch selbst
steuern.
Wenn Sie das selber steuern wollen, dann rufen Sie diesen Konstruktor
der Oberklasse mit super und dann in Klammern eben die Argumente für
den Konstruktor auf.
Sie erinnern sich vielleicht, es gab da praktisch eine analoge Sache
dazu.
Sie konnten einen Konstruktor aufrufen, wenn Sie mehrere in einer
Klasse hatten, indem Sie hier this, das Schlüsselwort, verwenden und
dann in Klammern die Argumente von dem Konstruktor hinschreiben, dann
wurde mit dieser this-Methode der Konstruktor mit den entsprechenden
Argumenten bezeichnet und analog dazu gibt es eben jetzt noch hier die
Möglichkeit mit super einen Konstruktor aus der Oberklasse aufzurufen.
Wenn Sie so etwas verwenden wollen, also explizit einen Konstruktor
der Oberklasse angeben, dann muss das immer die erste Anweisung in
Ihrem Konstruktor der abgeleiteten Klasse sein.
Also Sie können nicht erst Ihre eigenen Attribute in der abgeleiteten
Klasse initialisieren, dann super aufrufen, sondern super muss als
erstes dastehen.
Wenn Sie super nicht hinschreiben, wird von Java immer der
parameterlose Konstruktor der Oberklasse aufgerufen.
Also es entspricht dem, dass Sie einfach super mit Klammern, ohne dass
dann Argumente zwischen angegeben werden, aufrufen.
Wenn Sie keinen Konstruktor haben, dann wird dieser Default
-Konstruktor eingeführt von Java.
Wenn Sie eine abgeleitete Klasse haben, dann wird hier eben so ein
Konstruktor eingeführt wie dieses.
Es gibt keine Attribute in der Klasse selber, da sehen wir auch
nichts.
Und dann wird dieser Standard-Super-Konstruktor praktisch von Java
dazugefügt, der dann eben den Konstruktor der Oberklasse aufruft.
Und wenn Sie so eine ganze Klassenhierarchie haben, wo eben mehrere
abgeleitete Klassen untereinander sind, dann passiert es, dass bei
einem Konstruktor eben nach oben hin alle Konstruktoren aufgerufen
werden, bis zu ihrer allerobersten Klasse in der Hierarchie.
Okay, also wenn man so eine komplexere Hierarchie hat in der
Klassenhierarchie wie am Anfang, da hatten wir ja, glaube ich, Living
Organism ganz oben, dann Animal, Mammal, dann kam irgendwann mal Dog.
Das heißt, wenn man hier jetzt einen Konstruktor aufruft von Dog, dann
wird nach oben hin eben bis zu dieser Living Organism Klasse werden
sämtliche Konstruktoren aufgerufen.
Okay, und klar, dazu muss die Oberklasse einen Default-Konstruktor
besitzen, also der von Java automatisch dazugefügt wird, oder einen
Konstruktor ohne Parameter.
Falls das nicht der Fall ist, dann läuft die Initialisierung auch
nicht weiter an der Stelle, dann bricht die ab.
Gut, wenn es in einer Klasse keinen Konstruktor ohne Parameter gibt,
dann muss man in den Subklassen die Konstruktoren eben von Hand
schreiben, weil Java nur diesen Superkonstruktor ohne Parameter
einfügen würde.
Wenn es den nicht gibt, müssen sie eben von Hand, so wie hier oben mit
dieser Superanweisung, mit entsprechenden Argumenten für einen
Konstruktor der Oberklasse den aufrufen, um die zu initialisieren.
Gut, also soweit zu Konstruktoren.
Im Normalfall ist es so, dass da vieles eigentlich mit dieser Default
-Regel, dass dieser Superkonstruktor aufgerufen wird, schon ganz gut
funktioniert.
Wenn Sie spezielle Konstruktoren eben aufrufen wollen in Ihrer
Oberklasse, dann müssen Sie das von Hand mit entsprechenden Parametern
machen.
Im Zusammenhang mit der Vererbung gibt es noch einen zusätzlichen,
speziellen Sichtbarkeitsmodifier oder Modifikator.
Was kennen wir an Sichtbarkeitsmodifikatoren bisher?
Wir hatten Public, Private oder man konnte auch gar nichts
hinschreiben.
Und der wird jetzt im Zusammenhang mit der Vererbung nochmal
erweitert, um einen zusätzlichen Modifikator, der sich Protected
nennt.
Und wir können es mal hier anschauen, wenn wir diese Tabelle jetzt
erweitern, dann haben wir einmal hier eine zusätzliche Zeile für
diesen Protected-Eintrag.
Wir haben auch eine zusätzliche Spalte, wo eben diese Vererbungsdinge
mit reinspielen.
Jetzt haben wir kein Bild mehr.
Und da schauen wir mal kurz, was hat sich verändert.
Also Public bedeutet, eigentlich wie bisher, ist überall sichtbar.
Eine öffentliche Variable oder öffentliche Methode ist überall
sichtbar.
Protected heißt jetzt also geschützt.
Was man nicht ganz privat haben möchte, die Variable, auch nicht
öffentlich, sondern so eine Zwischenstufe, wie in Java Protected oder
geschützte Variable genannt, die ist dann sichtbar in der Klasse, im
Paket auch und auch in Unterklassen.
Das heißt, wenn Sie eine Methode oder ein Attribut haben wollen, das
nicht nur in Ihrer Klasse selbst, sondern auch in Unterklassen
sichtbar ist, dann verwenden Sie das Schlüsselwort Protected.
Also, schauen wir mal weiter, was haben wir hier noch.
Also wenn Sie gar nichts hinschreiben, dann ist das nur in der Klasse
und im Paket sichtbar, in Unterklassen nur, wenn Sie im selben Paket
sind.
Man könnte auch einen Haken machen, weil das eigentlich der Normalfall
ist.
Oder häufiger der Fall ist vielleicht, dass man den in anderen Klassen
verwendet.
Und Private ist nur in der Klasse selbst sichtbar.
Da haben wir die stärkste Einschränkung, wo so ein Private Attribut
oder Methode verwendet werden kann.
Aber das heißt zum Beispiel auch, wenn Sie jetzt so eine abgeleitete
Klasse von Ihrer Klasse zulassen wollen, die Sie implementiert haben,
dann sollten da die Attribute nicht Private sein, weil die sonst eben
nur von der Klasse selbst gelesen werden können.
Wenn die Attribute auch von den abgeleiteten Klassen gelesen werden
können, sollen oder geschrieben werden sollen, dann sollten Sie
Protected verwenden.
Also in Ihren eigenen Klassenimplementierungen für Attribute, Private
und Protected, kann da beides je nach Zweck verwendet werden.
Und Public hat man ja gesagt, das sollte möglichst nicht für Attribute
verwendet werden.
Das ist auch mal ein Mittel zur Realisierung des Geheimnisprinzips,
was eben jetzt angepasst wurde nochmal um die Verarbungsaspekte.
Es gibt noch einen Java-Operator, InstanceOf heißt der, den hatten wir
schon mal kurz kennengelernt, aber der ist im Zusammenhang mit
Verarbung der Haupteinsatzzweck.
Dieses B2 hat auch Attribute aus der Klasse Pinguin.
Also die Attribute, die werden immer praktisch mit dem New Operator
angelegt und sind danach auch immer verfügbar.
Das ist vielleicht eine interessante Frage noch, das stimmt, auch mit
Methoden, aber das sehen wir gleich noch im Zusammenhang mit dem Cast
-Operator.
Ich versuche es nochmal zu erwähnen, auf der nächsten Folie.
Also InstanceOf-Operator hat man gesagt.
Der im Zusammenhang mit Verarbung eben nochmal interessanter wird.
Mit InstanceOf konnte man ja prüfen, ob ein gegebenes Objekt einen
gegebenen Typ hat.
Und die Verwendung war so, wir schreiben hier eine
Objektreferenzvariable hin, InstanceOf und hier einen Klassenname.
Und konnten damit prüfen, ob dieses Objekt dieser Klasse gehört.
Also hier sehen wir so ein Beispiel, hier wird mInstanceOfDoc
verwendet.
Und hier haben wir als Objekt ein Objekt der Klasse, also der
statische Typ ist Memo.
Und bisher war es so, dass InstanceOf immer nur genau den einen Typ
abgeprüft hat, der eben hier dahinter gestanden ist, dieses Doc.
Im Zusammenhang mit solchen Klassenhierarchien und Vererbung ist es
so, dass hier mehr geprüft wird.
Also hier wird dann geprüft, ob diese Variable eben vom Typ Doc oder
einer abgeleiteten Klasse ist.
Und das kann man hier auch ganz geschickt verwenden.
Also wenn Sie jetzt an manchen Stellen eben doch sowas wie eine
Fallunterscheidung mal benötigen sollten, dann können Sie eben prüfen,
ist diese Variable M, also der dynamische Typ, das was hier wirklich
drinsteckt im M, ist das ein Objekt der Klasse Doc?
Und wenn ja, dann können Sie eben entsprechend diese Fälle hier
unterscheiden.
Das heißt, mit InstanceOf wird der dynamische Typ abgeprüft, nicht der
statische Typ.
Also deshalb, wenn Sie das jetzt aufrufen mit Cat zum Beispiel, gut,
das ist klar, das ist keine Instanz von Doc, also kommt daraus, es ist
not a dog.
Wenn Sie es aufrufen mit Dog, dann spielt jetzt eben dieser dynamische
Typ eine Rolle, also hier diese Variable Sie reinstecken, hat den
statischen Typ Memo, aber was hier geprüft wird, ist eben der
dynamische Typ, also ist der dynamische Typ InstanceOf Dog, das ist
hier der Fall, und deshalb ist dieser Ausdruck hier wahr und man
springt in diesen ersten Fall rein.
Ist Dog Memo, das wäre jetzt hier wieder nicht der Fall, weil es eben
keine Instanz von Dog ist.
Wenn Sie das mit Null aufrufen sollten, dann ist das auch nicht eine
Instanz von der Klasse Dog, also auch bei Null wird ein Forst
zurückgegeben.
Jetzt kommen wir zu dem Thema Typumwandlungen, der auch ein bisschen
mit Ihrer Frage zusammenhängt.
Also, was man machen kann, ist, dass man den statischen Typ einer
Variable zur Laufzeit verändern kann.
Oder was heißt verändern, also Sie können, schauen wir uns das
Beispiel mal an, Sie haben hier so eine Methode, die eben eigentlich
nur für Katzen richtig funktioniert, Sie haben aber als Parameter ein
Objekt vom Typ Zeugetier.
Und weil diese Methode eigentlich nur mit Katzenobjekten funktioniert,
prüfen Sie am Anfang ab, ob das der Fall ist, also ob dieses Objekt,
was Sie hier reinbekommen, was ja eben ein Objekt dieser Klasse oder
einer beliebigen abgeleitenden Klasse davon sein kann, prüfen Sie
nach, ist das zufälligerweise eben von dieser abgeleitenden Klasse Cat
ein Objekt, was Sie hier reinbekommen.
Wenn das der Fall ist, dann können Sie hier mit diesem Cast-Operator
den statischen Typ verändern und dann auf diesem Objekt die
entsprechende Methode aufrufen.
Und wozu braucht man das jetzt?
Also, wir haben gesehen, vieles hängt in Java vom dynamischen Typ der
Variable ab.
Also um zu entscheiden, wenn es mehrere Methoden gibt, welche jetzt
ausgewählt werden sollen, dann dynamischer Typ, dynamische Bindung ist
das, was verwendet wird.
Um aber festzustellen, welche Methoden überhaupt vorhanden sind und
welche aufgerufen werden können, unter anderem auch, welche Attribute
vorhanden sind, spielt der statische Typ eine Rolle.
Das heißt, wenn diese Meow-Methode eben in der Katzenklasse deklariert
ist, aber nicht in der Basisklasse davon, in Mammal zum Beispiel, dann
könnten Sie auf diesem Objekt M, was ja den statischen Typ Mammal hat,
nicht so eine Meow-Methode aufrufen.
Weil eben in dieser Klasse Mammal gar keine Meow-Methode zur Verfügung
steht.
Das heißt, welche Methoden Sie aufrufen können und auf welche
Attribute Sie zugreifen können, das hängt von dem statischen Typ ab.
Also hier mit dieser Variable M können Sie nur Methoden aufrufen, die
eben in dieser Klasse Mammal deklariert sind.
Die können dann zwar andere sein, wenn Sie die überschrieben haben,
was dann wirklich ausgeführt wird, aber Sie können eben nicht auf
Methoden zugreifen, die eben nur in der Subklasse definiert sind, weil
eben ansonsten dieser Methodenaufruf auch fehlschlagen könnte in
manchen Fällen.
Weil es eben ja zum Beispiel in der Dog-Klasse gibt es gar keine Meow
-Methode und wenn man dann einfach auf M, auf dem allgemeinen Fall
Meow, aufrufen würde, gäbe es gar keine Methode, die aufgerufen werden
könnte.
Und deshalb gibt es eben jetzt die Möglichkeit hier mit diesem Cast
-Operator, den statischen Typ zu verändern.
Von der Variable also, Sie ändern den statischen Typ von M auf Katze
ab und können dann eben diese Katzen-Methoden darauf aufrufen.
Also, Typecast ändert den statischen Typ eines Objekts.
Also Katze ist eine Katze, kann aber auch als Säugetier behandelt
werden, aber eben nicht zum Beispiel als Hund.
Und das deutet auch schon ein bisschen an, also so ein Typecast ist
etwas, was nicht in allen Fällen gut gehen muss.
Also Sie können so einen Typecast hinschreiben und der kann auch
fehlschlagen.
Okay, aber schauen wir uns erst einmal die Syntax an.
Eigentlich kennen Sie sie schon von früh aus, man schreibt den
Typnamen in Klammer, den neuen Typnamen, auf den das eben geändert
werden soll, auf dem ist ein Cast Operator und dahinter die Variable,
so wie Sie es eigentlich oben schon gesehen hatten.
Gut, und ich habe schon gesagt, das kann in manchen Fällen eben auch
schief gehen, das schauen wir uns jetzt auch noch ein bisschen genauer
an.
Also es gibt da Up- und Downcasts, wenn man es sich genau betrachtet.
Was sind Up- und Downcasts?
Upcast wäre eben eine Typveränderung, die in der Klassenhierarchie
nach oben geht, also eine Typumwandlung auf eine Superklasse.
Das ist relativ harmlos, also da kann nichts dabei schief gehen, wenn
Sie von einem gegebenen Objekt, also wenn Sie zum Beispiel als
Parameter in Ihrer Methode Dog haben und Sie verallgemeinern den Typ
auf Mammal, dann kann da nichts schief gehen, weil Sie wissen, das
Objekt muss immer diesen allgemeineren Typ haben, wegen dieser
Isserbeziehung.
Und das wird auch von Java automatisch durchgeführt.
Also wenn Sie eine Methode aufrufen, wo eben ein Parameter einen
allgemeineren Typ hat, dann wird der automatisch angepasst.
Downcast, das ist das Kritischere, das ist die Typumwandlung von einer
allgemeinen in eine speziellere Klasse.
Das hatten wir auf der letzten Folie gesehen, wo eben diese statisch
vom Typ Mammal deklarierte Variable auf einmal in eine Katzenklasse
umgewandelt werden sollte.
Das wäre ein Downcast.
Und Downcast ist kritisch, also der kann eben auch schief gehen, wenn
eben dieses Objekt, was in die Methode reingesteckt wurde, nicht dem
Typ entspricht.
Also Sie bekommen zum Beispiel in diese Methode der letzten Folie kein
Katzenobjekt reingesteckt, sondern ein Hundeobjekt, versuchen das dann
in so ein Katzenobjekt zu casten mit diesem Typecast, das kann dann
eben schief gehen, weil das einfach eben nicht der passende Typ ist.
Deshalb gibt es da eben Restriktionen, also zum einen muss dieser
Downcast immer explizit angegeben werden, kann eben auch fehlschlagen,
deshalb sollte man immer vorher prüfen mit InstanceOf, ob eben die
Variable, die Sie jetzt versuchen auf eine Subklasse zu casten, ob die
wirklich auch den passenden Typ hat, also den passenden dynamischen
Typ.
Und nur dann sollten Sie so ein Cast durchführen.
Hier waren wir, genau.
Also immer mit InstanceOf vorher prüfen, bevor man so ein Cast
wirklich durchführt und das kann eben auch zu einem Laufzeitfehler
führen, wenn der Typ eben nicht passt von der Variable, da bekommen
Sie dann, das kennen Sie wahrscheinlich auch schon aus Ihrem
Programmieralltag bisher, dass dann so eine Exception auftreten kann,
in dem Fall wäre das eine ClassCastException, die hier passiert.
Gut, Java hat schon eine ganze Reihe von Klassen vordefiniert, die in
den Bibliotheken zu Java mitgeliefert werden und eine besonders
wichtige Klasse dabei ist die Klasse Object.
Und diese Klasse, die ist in dem Paket Java lang vorhanden und diese
Object -Klasse ist automatisch immer Oberklasse von allen Klassen, die
Sie anlegen.
Also auch ohne, dass Sie irgendwas hinschreiben, sind alle Ihre
Klassen immer von Object abgeleitet.
Also entweder direkt, wenn Sie eben gar nichts hinschreiben, wenn Sie
keine Extenz angeben, dann werden die direkt von Object abgeleitet.
Wenn Sie von einer eigenen Klasse ableiten, dann ist diese bereits
vorhandene eigene Klasse ja auch schon von Object abgeleitet und dann
sind Sie praktisch indirekt von Object abgeleitet.
Also alle Klassen in Java von Object abgeleitet.
Da haben Sie bisher nicht viel davon gemerkt, weil das eben alles
versteckt ist und Sie das nicht explizit angeben mussten.
Und dadurch, dass alle Klassen von Object abgeleitet sind, gibt es
auch eben diese eine Wurzel der Klassenhierarchie.
Also Object steht immer ganz oben und von diesem Object ist alles
abgeleitet.
Da kann man sich fragen, was steht in den Object drin, oder was bringt
es, so eine Klasse zu verwenden.
Es gibt eine Reihe von Methoden, die darin schon deklariert sind, die
man selber im Bedarf auch verwenden kann oder überschreiben kann.
Es gibt einmal diese Equals-Methode, die ein Objekt mit einem anderen
Objekt vergleicht.
Es gibt die ToString-Methode, um eine Variable auszugeben.
Die ToString kennen Sie schon, aber das hat eine relativ einfache
Default -Implementierung für Objekte, die nur so einen Hash-Wert
ausgibt.
Im Allgemeinen dient das dazu, Ihre Objekte in eine String
-Repräsentation zu überführen.
Dann gibt es eine Clone-Methode, die Ihre Attribute kopiert, also eine
1 -zu-1-Kopie des aktuellen Objekts anlegt.
Und diese Methoden muss man im Normalfall eben anpassen für die
eigenen Klassen.
Also wenn Sie eine Equals, ToString oder Clone-Methode verwenden
wollen, müssen Sie die im Normalfall für Ihre Klassen anpassen.
ToString haben Sie vielleicht schon gesehen, Equals haben Sie
möglicherweise auch schon verwendet.
Zu Equals gleich noch ein bisschen mehr.
Aber schauen wir uns mal an, was Java sonst noch an Objekten zur
Verfügung stellt.
Also wie gesagt, ganz oben in der Java-Klassenhierarchie ist immer
dieses Object, was automatisch auch für Ihre Klassen immer die
Basisklasse ist, wenn Sie nichts anderes angeben.
Da gibt es verschiedene Klassen, einmal gibt es so eine Boolean
-Klasse, dann eine Character-Klasse, da ist das R abgeschnitten.
Dann Math haben Sie auch schon gesehen, Number, String,
StringBuilderSystem und noch viele, viele weitere Klassen.
Bei Number gibt es zum Beispiel jetzt auch wieder Byte, Double, Float,
Integer, Long, Short.
Eigentlich für die ganzen elementaren Datentypen gibt es dann nochmal
solche entsprechenden Klassen hier, die dazu gehören.
Und das sind sogenannte Wrapper-Klassen, das sehen wir später auch
noch, möglicherweise heute noch in der Vorlesung, eventuell auch erst
nächstes Mal, wozu die verwendet werden.
Aber ansonsten gibt es eben hier Mathematik-Funktionen, String
-Funktionen, es gibt Funktionen für Netzwerkkommunikation, also ein
ganz breites... also die sind nicht in Java Lang drin, aber Java Lang
umfasst diese und noch etwas mehr.
Gut, wir haben gesehen, eine Methode, die in Object vorhanden ist, ist
diese Equals-Methode.
Und die überschreibt man doch relativ häufig, wenn man die eigenen
Objekte eben auch auf Gleichheit testen möchte.
Und das ist nicht ganz einfach, man kann da vielleicht relativ leicht
auch Fehler machen bei der Equals-Methode, deshalb schauen wir uns das
ein bisschen genauer an.
Und wir beginnen damit, dass wir uns mal anschauen, wie denn in dieser
Java -API, also in der Dokumentation oder in dem Application
Programmer Interface, also in der API, was da drin steht zu der Equals
-Methode.
Also in der Object-Klasse ist diese Equals-Methode deklariert und da
steht eben drin, was die machen soll.
Und das ist relativ kompliziert, wie das auch beschrieben ist, also
das ist wirklich nicht so ganz einfach.
Also das ist der englische Text, der direkt von der Seite von Oracle
kommt.
Also die Equals-Methode, was macht die?
Indicates whether some other object is equal to this one.
Das ist das wenig Überraschende, also es prüft, ob zwei Objekte gleich
sind.
Dann wird es interessanter.
The Equals-Method implements an equivalence relation on non-null
object references.
Also eine Äquivalenzrelation soll sich daraus ergeben, aus dieser
Äquivalenzmethode, die wir hier deklarieren.
Und für Nicht-Null-Objekte, also Null-Objekte sind ein Spezialfall.
Und was heißt jetzt, dass es eine Äquivalenzrelation sein soll?
Kennen Sie vielleicht aus der Mathematik schon, haben Sie den Begriff
für Äquivalenzrelation gehabt?
Eine Äquivalenzrelation hat bestimmte Eigenschaften, sie ist reflexiv,
ich mache die gleich mal alle hin, reflexiv, symmetrisch und
transitiv.
Also das sind die Eigenschaften von einer Äquivalenzrelation.
Und was bedeutet das jetzt?
Reflexiv heißt, dass wenn Sie diese Methode mit dem selben Objekt
aufrufen, dann muss das wahr sein.
Also x gleich x soll gelten.
Also wenn Sie x equals wieder mit x aufrufen, dann muss da 2
auskommen.
Das ist eine Grundeigenschaft, die ist jetzt nicht sehr überraschend,
aber da müssen Sie eben bei Ihrer eigenen Implementierung darauf
achten, dass das wirklich der Fall ist.
Sie sollte symmetrisch sein, das heißt, wenn Sie x equals y aufrufen
und y equals x, dann sollte jeweils dasselbe rauskommen.
Und dann schließlich transitiv, das heißt, wenn Sie drei Variablen x,
y und z haben und Sie haben, dass sowohl x equals y wahr ist, als auch
y equals z wahr ist, das war x gleich y, y gleich z, dann sollte eben
auch dieser Sprung gelten, wenn man von x direkt nach z geht, dass die
dann auch gleich sind.
Das ist diese Transitivität.
Also die Dinge sollten Sie sicherstellen bei Ihrer Implementierung,
dass die alle gelten.
Dann gibt es noch weitere Dinge, die von der Äquivalenzrelation nicht
direkt dazugehören.
Das ist jetzt programmierabhängig und nicht von dem mathematischen
Begriff Äquivalenzrelation abgedeckt, die sogenannte Konsistenz.
Was ist das hier?
Wenn Sie mehrmals hintereinander x equals y aufrufen, sollte da immer
dasselbe rauskommen.
Also diese Equals-Methode sollte diese Objekte nicht modifizieren.
Zumindest in den Dingen, die eben für die Gleichheit eine Rolle
spielen.
Also mehrfach Aufruf hintereinander sollte immer das gleiche Ergebnis
geben.
Das ist in der Mathematik kein Problem bei einer Äquivalenzrelation,
weil sich da die Dinge nicht verändern können.
In Programmen haben sie eben Zustand und Zustandsänderungen, deshalb
könnte auch so eine Equals-Methode da was dran ändern.
Und dann noch den Spezialfall 0.
Für 0 sollte die Methode immer Force zurückgeben.
Also wenn Sie auf einem bestehenden Objekt die Methode aufrufen, das
kann dann ja nicht 0 sein, sonst könnten Sie die Methode nicht
aufgerufen haben, und Sie vergleichen das mit 0, dann muss da immer
Force rauskommen.
Also das sind die Sachen, die Sie implementieren sollten.
Und dann ist die Frage, was macht die Standardimplementierung Java,
wenn Sie die nicht überschreiben, die Equals-Methode auf Ihren eigenen
Klassen, die prüft, das steht ja unten auch im Text schon ein bisschen
lang, nur für praktisch die Referenzgleichheit.
Also wird geprüft, x gleich y ist dann gleich, wenn es eben dieselben
Objektreferenzen sind.
Also wenn es wirklich dieselben mit new angelegten Objekten sind, nur
dann gibt es true zurück.
Das ist eigentlich selten, dass man das haben möchte.
Das kriegt man eben auch mit dieser, wenn man hinschreibt, x gleich
gleich y für Objektreferenzen, und deshalb sollte man die Methode eben
auch überschreiben im Normalfall.
Also nochmal, wenn Sie diese Methode implementieren, was man tun
sollte, sollte man eben auf die Eigenschaften achten, diese
Reflexivität, Transzidivität, Symmetrie und Konsistenz.
Also wenn Sie so eine Equals-Methode selber schreiben, was machen Sie
dabei?
Erstmal die Syntax muss passend sein, also die Methode muss ein
Boolean zurückgeben und der Typ des Parameters sollte Object sein.
Und da ist es dann auch häufig so, dass man dann, wenn man eine solche
Gleichheit eben erstmal mit Instance aufprüft, ob dieser Parameter,
der übergeben wird, eben auch der Klasse des Objektes entspricht, auf
dem es aufgerufen wird.
Da kann man eben das Instance-Aufprüfen gut verwenden und nur dann
macht man die Prüfung.
Ansonsten, wenn es sich um Objekte unterschiedlicher Klassen handelt,
können Sie gleich Force zurückgeben.
Also Reflexivität prüfen Sie so, liefert die Methode zu, wenn Sie für
den Parameter Object-This einsetzen.
Also wenn Sie This equals This aufrufen, muss To rauskommen.
Symmetrie, wenn Sie This und Object vertauschen, kommt das gleiche
raus.
Transitivität, zum Beispiel bei Gleitkommazahlen kann das ein Problem
sein, wenn man da so einen Y-Umgebungstest verwenden möchte, dass das
dann nicht funktioniert.
Da muss man manchmal ein bisschen aufpassen.
Und Konsistenz bedeutet, die Methode darf eben nicht den Teil des
Objekts verändern, der für Gleichheit relevant ist.
Und wenn Sie das alles beachten, dann passt Ihre IVUS-Methode.
Aber im Normalfall hat man da eigentlich eine relativ gute Vorstellung
davon, wie das implementieren ist, sodass diese Dinge eigentlich fast
schon automatisch gelten, wenn man einfach eben so einen
Gleichheitsbegriff schon im Kopf hat, der diese Dinge eben
berücksichtigt.
Gut, jetzt gibt es noch eine Möglichkeit, die Vererbbarkeit
einzuschränken.
Das geht mit dem Schlüsselwort FINAL.
FINAL haben wir in einem anderen Zusammenhang schon gesehen.
Bei Attributen hat man es gesehen, dass es einen konstanten Wert
anzeigt, der nicht verändert werden kann, also um konstanten zu
deklarieren.
Bei Parametern gab es das auch.
Das bedeutet, unverändert kann nicht verändert werden im
Methodenrumpf.
Und jetzt gibt es eben noch eine dritte Verwendung von FINAL im
Zusammenhang mit Vererbung.
Wenn Sie das bei einer Klasse davor schreiben, also FINAL Class
irgendwas, dann bedeutet das, von dieser Klasse können keine weiteren
Klassen abgeleitet werden.
Das heißt, man kann für die eigenen Klassen verbieten, dass von dieser
Klasse weitere Klassen abgeleitet werden.
Das geht eben auch mit diesem FINAL.
Wozu möchte man so etwas machen?
Es gibt eines, das ist Sicherheit, andere Performance.
Sicherheit bedeutet, dass wenn jemand annimmt, dass er mit einem
Objekt von ihrer Klasse arbeiten kann, wenn sie Vererbung haben, dass
passieren kann, dass er in Wirklichkeit mit einem davon abgeleiteten
Objekt arbeitet, wenn man das nicht wirklich unterscheiden kann oder
wenn man es nicht berücksichtigt speziell, das ist dieser
Sicherheitsaspekt.
Performance kann auch in manchen Fällen eine Rolle spielen, wenn man
verhindern möchte, dass man diese ganze Klassenhierarchie für dieses
dynamische Binden zum Beispiel durchgehen muss.
Dann kann man das ja auch verbieten.
Das ist zum Teil auch gemacht.
Also ein Java-Lang-String zum Beispiel ist eine Klasse, die sich nicht
weiter spezialisieren lässt.
Also davon können Sie keine weiteren Klassen ableiten.
Es ist dann damit eben nicht mehr möglich, die Methoden, die in diesen
Klassen drinstehen, weiter zu überschreiben mit anderen Dingen.
Und damit wird das Verhalten praktisch fixiert und deshalb auch diese
erhöhte Sicherheit.
Also Sie können dann für Ihre Klassen eben dieses Final davor
schreiben, Final Class irgendwas, dann haben Sie diese nicht weitere
Verirrbarkeit ab dieser Stelle.
Es gibt noch ein weiteres Schlüsselwort, das Sie davor schreiben
können für Ihre Klasse, das heißt Abstract.
Und das spielt im Zusammenhang mit sogenannten abstrakten Klassen eine
Rolle.
Was ist eine abstrakte Klasse?
Eine abstrakte Klasse ist eigentlich eine Klasse, von der keine
Objekte instanziiert werden können.
Also kein U, das ist Oberklasse, das sollten Sie sich eigentlich
wegdenken.
Also einfach Klasse ohne direkt instanzierte Objekte.
Und man kann sich fragen, warum braucht man sowas?
Wieso möchte man irgendwie angeben können, eine Klasse, von der man
gar keine Objekte direkt anlegen kann?
Und was man damit erzwingen möchte, ist, dass man die Klasse eben
nicht direkt verwendet, sondern von dieser Klasse eigene Klassen
ableitet, darin dann eben bestimmte Methoden implementiert.
Und diese abstrakte Klasse dient mehr oder weniger als Schnittstelle,
um so allgemeinere Konzepte zu beschreiben.
Also in dieser abstrakten Klasse können abstrakte Methoden auftauchen,
die sind auch wieder mit so einem Abstract-Schlüsselwort
gekennzeichnet.
Und von diesen Abstract-Methoden wird eben keine Implementierung
eingegeben, sondern da wird die Implementierung offengelassen und eben
auf die Unterklassen verlagert.
Die Unterklassen müssen das dann implementieren, diese Methode.
Okay, als Attribute-Konstruktion eigentlich fast alles läuft wie in
normalen Klassen, bis auf eben diese abstrakten Methoden.
Und die abstrakten Methoden müssen, wie gesagt, in Unterklassen
überschrieben werden.
Und wenn Sie dann eben diese abstrakte Klasse irgendwo oder diesen als
statischen Typ verwenden, dann wird eben über die dynamische Bindung
die passende Methode aus der Unterklasse ausgewählt.
Also hier nochmal die Syntaxe, Sie schreiben in der Abstract-Klasse
irgendwas und in dieser Klasse können einzelne Methoden als abstract
deklariert sein, die dann keine Implementierung haben.
Also hier zum Beispiel, wenn wir hier ein Säugetier nochmal haben,
wenn wir das als abstrakte Klasse deklarieren, dann würden Sie damit
anzeigen, es kann nicht direkt ein Objekt dieser Klasse, Säugetier,
angelegt werden.
Und damit wollen wir eben erzwingen, dass Objekte immer speziellere
Tiersorten praktisch angeben.
Also das heißt, man muss dann so eine Klasse wie Katze oder Hund
ableiten, kann von denen Objekte anlegen, aber eben nicht von diesem
übergeordneten Objekt Säugetier.
Damit kann man eben erzwingen, dass der Programmierer eben immer
speziellere Objekte anlegt.
Also Säugetier direkt darf er dann eben nicht, sondern muss sich eben
für ein konkretes entscheiden.
Man kann dann aber gemeinsame Funktionalität eben doch zusammenfassen
in so einer übergeordneten Klasse, wie hier diesen Mämmel eben, dass
man bestimmte Methoden eben nicht jedes Mal neu programmieren muss.
Also wir haben weiterhin die Vorteile der Vererbung noch, als zum
Beispiel hier so etwas, dass ein Säugetier eben, oder dass es hier
Geburt gibt, das kann hier allgemein implementiert sein, aber eine
andere Methode, Speak, die möchte man auch irgendwie für alle Tiere
deklariert haben, aber noch nicht angeben hier in dieser abstarken
Klasse, wie die Implementierung aussehen soll.
Und dann würde man eben hier so eine Methode Speak reinschreiben, wo
jedes Tier sich eben irgendwie lautlich äußern kann.
Und durch Abstract wird eben angezeigt, hier ist noch keine
Implementierung vorhanden, sondern die muss dann für den Spezialfall
vorgenommen werden.
Und genau, also in erster Linie dient es dazu, solche abstarken
Klassen, um so eine allgemeine Schnittstelle zur Verfügung zu stellen,
für einen allgemeinen Fall.
Also hier eben für das allgemeine Fall Säugetier, da wollen wir eben
bestimmte Dinge schon mal festschreiben, die es hier geben soll.
Also das wissen wir, da gibt es eben so eine Speak-Methode drauf, es
gibt diese Give Birth-Methode drauf und dieses Attribut, aber manche
Sachen sollen eben noch nicht implementiert werden.
Okay, wir sehen nachher in der vorliegenden Lesung noch eine Variante
davon, wo das noch weitergeführt wird, das nennt sich dann Interface
in Java.
Das ist verwandt mit diesem Konzept der abstrakten Klassen.
Und da wird es vielleicht noch ein bisschen klarer, auch wozu man
diese abstrakten Klassen verwendet.
Also damit, wie gesagt, man kann eben dann von dieser abstrakten
Klasse keine Objekte mit New anlegen, und damit kann man eben so etwas
erzwingen, dass man keine reine Säugetier-Objekte haben möchte,
sondern immer nur konkrete Unterklassen.
Gut, damit sind wir mit diesem Kapitel Vererbung am Ende.
Wir machen gleich weiter mit anderen Dingen.
Und zum Abschluss habe ich jetzt noch ein paar Quizfragen wieder für
Sie.
Versuchen wir da mal umzuschalten auf den anderen Bildschirm.
Okay, also die Frage, da ist noch eine schöne Hintergrundsache
aufgetaucht.
Also ich hoffe, man kann es trotzdem lesen hier.
Also die Frage ist hier, erbt eine Unterklasse sowohl Attribute als
auch Methoden?
Relativ einfacher Einstieg.
Genau, dann starte ich das mal hier.
Okay, das heißt jetzt können Sie sich für eine dieser Antworten
entscheiden.
Also wieder wie gewohnt einwählen auf der Seite msocrative.com, Raum
ist Prog 1314.
Und die Frage, erbt eine Unterklasse sowohl Attribute als auch
Methoden?
Mögliche Antworten sind Nein, nur Attribute werden vererbt.
Nein, nur Methoden werden vererbt.
Ja, sämtliche Variablen und Methoden werden vererbt.
Ja, aber private Methoden und Attribute werden nicht vererbt.
Oder keine Ahnung.
Wir haben schon so ein paar Eingaben, ich warte mal, ob noch ein paar
dazukommen.
Ja, ich sehe schon, E ist relativ beliebt, aber die anderen Antworten
sind besser noch.
Wir sehen auch schon, für D gibt es hier eine relativ klare Mehrheit
schon.
Das heißt, ja, aber private Methoden und Attribute werden nicht
vererbt.
Das ist auch die richtige Antwort.
Genau, also sowohl Methoden als auch Attribute werden vererbt, aber
private Sachen sind eben beschränkt auf die Klasse, in der sie
deklariert sind.
Wenn die auch vererbt werden sollen, müssen sie Protected verwenden.
Gut, dann habe ich eine zweite Frage hier, die können Sie schon mal
lesen.
Ich breche das jetzt trotzdem mal hier ab.
Und wir kommen zur nächsten Frage.
Das ist jetzt schon ein bisschen Konzeptuelles eher.
Was ist kein Vorteil der Verwendung von Vererbungen?
Also irgendwie sind da wahrscheinlich vier Vorteile dabei und eins ist
eben keiner davon.
Und schauen wir uns mal an, Code, der von mehreren Klassen verwendet
wird, muss nur einmal geschrieben werden.
Ja, könnte ein Vorteil von Vererbung sein.
Verweiterung um neue Programmeigenschaften wird vereinfacht.
Programmcode kann einfacher wiederverwendet werden.
Abgeleitete Klassen sind unabhängig von der Basisklasse.
Programmcode ist besser strukturiert.
Ja, da gibt es schon ein relativ klares Ergebnis hier.
D sagen die meisten von Ihnen genau.
Abgeleitete Klassen sind nicht unabhängig von der Basisklasse, sondern
hängen sogar relativ stark von der Basisklasse ab, so wie sie eben die
ganzen Attribute und Methoden übernehmen.
Die anderen Sachen treffen alle zu.
Also Erweiterbarkeit, Code kann wiederverwendet werden.
Und das Ergebnis ist hier auch relativ klar.
Noch eine dritte Frage habe ich für Sie.
Gut, was haben wir jetzt?
Dritte Frage, kann eine abstrakte Klasse sowohl abstrakte als auch
nicht abstrakte Methoden enthalten?
Das hatten wir relativ kurz, aber Sie können vielleicht von Beispielen
und was Sie so vermuten, raten, was für sinnvoll ist, oder Sie wissen
es vielleicht auch schon.
Nein, alle Methoden müssen als abstrakt deklariert sein.
Nein, keine der Methoden darf als abstrakt deklariert sein.
Also es geht darum, genau, kann man ihn abstrakten lassen und ihn
mischen zwischen abstrakten und nicht abstrakten Methoden?
Ja, aber abstrakte Methoden werden nicht vererbt.
Ja, und sämtliche Methoden werden vererbt.
Ja, aber abstrakte Methoden dürfen nicht überschrieben werden.
Da haben wir auch ein relativ klares Ergebnis, schon wieder D.
Ist richtig, genau, ja, und sämtliche Methoden werden vererbt.
Also Sie können mischen, abstrakte, nicht abstrakte Methoden.
Abstrakten sind die, wo Sie eben keine Implementierung vorgeben, wo
das in einer abgeleiteten Klasse passieren muss.
Die nicht abstrakten Methoden, da liegt eine Implementierung vor, das
ist auch in Ordnung und die können auch entsprechend erweitert werden.
Gut, gibt es da gerade noch Fragen dazu?
Die Frage war, kann man irgendwie an zwei höher gehen in der
Hierarchie?
Das ist eine gute Frage, ich weiß ehrlich gesagt nicht genau, was
passiert, wenn Sie super, super hinschreiben, ich vermute mal, dass
das geht.
Oder weiß das jemand von Ihnen zufällig gerade?
Sonst im Zweifelsfall müssen Sie es mal ausprobieren.
Also es gibt, glaube ich, wenige Fälle, wo man sowas machen möchte.
Eigentlich ist es häufig im Konstruktor, aber ansonsten, genau, wenn
Sie Zugriff auf Attribute zum Beispiel haben oder Methoden mit super,
super, probieren Sie mal aus, ob es geht, ich weiß es gerade auch
nicht.
Gibt es sonst noch Fragen dazu?
Gut, haben wir keine, dann schalten wir hier wieder um.
Ja, wir haben hier leider immer noch den störenden Hintergrund.
Was sagt er uns eigentlich hier?
Okay,
also ich weiß nicht, ob ich da irgendwie Enter drücken kann, aber
wahrscheinlich muss irgendwer mal jemand hier die Lampe austauschen,
dass das verschwindet.
So, dann können Sie vielleicht hier drüben schauen.
Das Thema, was wir uns jetzt im Rest der Vorlesungsstunde heute noch
anschauen, ist sehr stark verwandt mit dem, was wir jetzt in Kapitel 8
hatten, also mit der Verarbung.
Und zwar geht es um Interfaces und Generics.
Und Interfaces sind sehr ähnlich zu den abstrakten Klassen, die wir
uns gerade angeschaut hatten.
Also was ist ein Interface und wozu wird das verwendet?
Und ich möchte es mit einem Beispiel Ihnen zeigen, wo das sinnvoll
sein kann.
Also wir hatten ja so eine Vector-2D-Klasse, die praktisch Punkte im
zweidimensionalen Raum implementiert.
Und jetzt könnte man auf die Idee kommen, dass man das ja auch in
Polarkoordinaten implementieren kann, wenn das vielleicht für manche
Sachen sinnvoller ist.
Also Polarkoordinaten bedeutet eben nicht, dass sie X und Y Wert haben
wie bei kathesischen Koordinaten, sondern dass sie einen Abstand vom
Ursprung R haben und einen Winkel, der eben von der X-Achse aus
gemessen wird, im Gegenuhrzeigersinn, der angibt, wie gedreht dieser
Punkt nach links ist.
Das sind Polarkoordinaten.
Und wenn Sie so eine Polarkoordinaten-Klasse schreiben für Punkte
wieder, also ein Punkt in Polarkoordinaten, gäbe es da eben die
Attribute, die beiden Phi für den Winkel, R für den Abstand,
Konstruktor und dann eine ganze Reihe von Methoden, die man eigentlich
auch für andere Dinge schreiben, für diese kathesischen Koordinaten
schreiben kann.
Also verschieben um einen Wert, rotieren um einen Winkel, rotieren um
einen Punkt, dann solche Distanzmethoden und Gleichheitsmethoden.
Da sehen Sie auch, diese Iquus-Methode, das stimmt hier schon mal, das
sollte man eigentlich besser hier schreiben, dass hier Object
dransteht und nicht PolarPoint, sonst überschreibt man eben nicht die
Methode aus Object.
Aber ansonsten sehen Sie, also hier haben wir eben jetzt praktisch
eine zweite Implementierung von unserer Punktklasse, die aber anders
eben abläuft.
Also die ist eben jetzt nicht auf den kathesischen Koordinaten
basiert, sondern auf den Polarkoordinaten.
Trotzdem ist ein Großteil gleich.
Das heißt, diese ganzen Methoden, die wir hier unten sehen, dieses
Shift -Rotate-Distance-To, die sind genau dieselben Methoden, die Sie
eigentlich auch für Ihre kathesischen Koordinaten haben wollen.
In vielen Fällen ist es eigentlich so, dass Sie sich für die
Implementierung der Klasse gar nicht so stark interessieren.
Also ob jetzt zum Beispiel ein Punkt hier als Polarkoordinaten oder
als kathesische Koordinaten implementiert ist, oder wie der
Programmierer das jetzt genau gemacht hat, interessiert Sie gar nicht
so stark, sondern was für jemand, der die Klasse verwendet, wichtig
ist, sind eigentlich hier diese Methoden, die Sie darauf aufrufen
können.
Und Interfaces dienen jetzt eben dazu, um von dieser konkreten
Implementierung zu abstrahieren und nur auf diese
Schnittstellenmethoden, also die Methoden, die eben nach außen
öffentlich bereitgestellt werden, auf die ein großes Gewicht zu legen.
Und das ist noch eine kleine Rennbemerkung, also das, was bisher
Vector2D heißt, um das jetzt eben unterscheiden zu können von dem
PolarPoint, nenne ich das in Zukunft so einfach kathesischer Punkt,
also Cartesian Point.
Also Hauptverwendung des Interfaces als Schnittstellenbeschreibung,
also das gemeinsame Verhalten von zwei Klassen, die unterschiedlich
implementiert sein können, aber eigentlich das gleiche bezwecken wie
diese beiden Punktklassen.
Dieses gemeinsame Verhalten soll eben über eine Schnittstelle
beschrieben sein.
Also wenn wir zum Beispiel so eine Liste von Punkten implementieren
wollen, da soll dann eben beides drinstecken können, sowohl diese
kathesischen als auch diese Polarkoordinaten.
Und was man dann machen kann, ist, dass man so ein Interface
definiert, was eben diese Funktionalität verallgemeinert von beiden
Klassen.
Und dieses Interface nennen wir dann eben einfach nur Point, wo wir
eben weglassen, ist das kathesisch oder ist das Polarkoordinate.
Das heißt, von den Implementierungsdetails abstrahieren wir hier und
haben nur noch das Gemeinsame im Interface festgehalten.
Also Interface enthält jetzt hier alle Public-Methoden, aber ohne
Implementierung vorzugeben.
Das heißt, so ein Interface könnte dann zum Beispiel so aussehen, man
hat diese Shift-Methode, die beiden Rotationsmethoden, dann die
Distance und so weiter, aber eben gar nichts mehr, was irgendwie auf
die Implementierung hindeutet.
Also Sie haben keine Attribute mehr hier drinstehen, sondern Sie sehen
praktisch nur noch, wie kann ich die Klasse verwenden.
Und das ist das, was über so ein Interface eben angezeigt werden soll,
also dieses Verwendungsverhalten von der Klasse festzulegen.
Und dann gibt es eben speziellere Klassen, die dieses Verhalten,
praktisch auch diese Schnittstelle implementieren.
Also es gibt dann eben zum Beispiel diese kathesische Punkt- und
Polarkoordinaten -Klassen, die praktisch diese Funktionalität zur
Verfügung stellen.
Und man kann dann in der Liste zum Beispiel eben verschiedene Dinge
reinstecken, die eben alle Punkte sind, aber unterschiedliche
Implementierungen haben.
Okay, und was wir dazu machen müssen, ist, dass dann eben diese
Klassen, die die Funktionalität zur Verfügung stellen, die müssen dann
mit diesem Interface irgendwie in Bezug gesetzt werden.
Und dieser Bezug nennt sich das Implementieren dieses Interface und
entsprechend heißt auch das Java-Schlüsselwort dann Implements.
Also wir haben diese allgemeine Schnittstelle Point und wir wollen
irgendwie anzeigen, dass eben jetzt diese Klasse, zum Beispiel mit
kathesischem Punkt, dass die jetzt dieses Interface bereitstellt.
Das heißt, die Funktionalität, die im Interface beschrieben ist, wird
von dieser Klasse zur Verfügung gestellt.
Und das wird eben über dieses Implements angezeigt.
Also hier oben, diese Klasse stellt eine Implementierung bereit für
dieses Punkt-Interface.
Da muss jetzt eben auch dann konkret angegeben werden, wie sehen die
Attribute aus, wie ist was programmiert.
Aber die gemeinsame Funktionalität, wie gesagt, ist eben hier in
diesem Point-Interface beschrieben.
Die ist eben für beide gleich und die Implementierung unterscheidet
sich dann in den spezielleren Klassen.
Was muss man machen?
Man muss alle Methoden, die in dem Interface Point vorkommen, müssen
implementiert werden von den Klassen.
Also man kann nicht nur teilweise die Methoden implementieren, sondern
sämtliche Methoden, die in dem Interface genannt werden, müssen
implementiert sein.
Weil sich jemand, der zum Beispiel über das Interface nachher auf ihre
Klasse zugreift, also über diese allgemeine
Schnittstellenbeschreibung, muss sich eben darauf verlassen können,
dass eben alles, was da drin steht an Funktionalität, auch wirklich
von ihrer Implementierung geliefert wird.
Also die müssen alle implementiert sein, müssen auch public sein,
damit man da wirklich darauf zugreifen kann in ihrer Klasse.
Und in Java ist das so realisiert, dass auch diese Interfaces selber
als Typen aufgefasst werden.
Das heißt, die können sie auch als Typ in ihrem Programm verwenden,
Interfaces.
Sie können in einer Methode zum Beispiel verlangen, dass ein Parameter
den Typ Point hat, wobei Point ein Interface wäre.
Und das bedeutet dann eben, in ihrem Programmcode verlangen sie nur,
dass diese Schnittstelle von einer Klasse irgendwie implementiert
wird.
Okay, also Interface-Sammlung von Methodensignaturen ohne
Implementierung.
Interface gibt eben nur an, was ist die Funktionalität, was ist die
Schnittstelle, die hier unten eben zusammengefasst wird und die von
bestimmten Klassen, die das dann implementieren, eben bereitgestellt
wird.
Also nur Methodensignaturen, keine Implementierung, legt Methodennamen
fest und die Parametertypen, aber die Implementierung ist noch offen.
Also über Implementierung sagt ein Interface überhaupt nichts aus.
Okay, also das sind praktisch wie Klassen, ohne Attribute und ohne
Implementierung.
Und wie gesagt, es kann auch selbst zur Typisierung verwendet werden.
Also eine Klasse C, die ein Interface I implementiert, ist ein Subtyp
von I.
Also das ist praktisch so eine Klasse, die Interface implementiert
ist, wie eine abgeleitete Klasse von dem Interface und kann auch fast
analog verwendet werden.
Okay, Interface wird von einer Klasse implementiert, indem man für
jede Methode eine Implementierung angibt und eben mit diesem
Schlüsselwort Implements das nochmal verwendet.
Und was auch geht, ist, dass Interfaces voneinander arbeiten können.
Das ist jetzt wieder ein bisschen verrutscht hier.
Also wenn Sie ein Interface I haben, was eben diese Methode Fu zur
Verfügung stellt, können Sie davon ein weiteres Interface ableiten,
auch wieder mit dem Schlüsselwort Extents, das eben dann alle
Funktionalität des Basisinterfaces zur Verfügung stellt, plus das, was
hier eben noch an Methodensignaturen vorgegeben wird.
Das heißt, wenn jetzt eine Klasse dieses Interface J implementieren
wollte, müsste sie eben diese Methode zur Verfügung stellen, plus alle
Methoden, die in I vorhanden sind.
Also sowohl Fu als auch Bar müssten in einer Klasse implementiert
sein, die dieses Interface J implementiert.
Okay, also Syntax hat man eigentlich schon gesehen, so grob.
Schauen wir uns aber jetzt noch im allgemeinen Fall an.
Ähnlich wie eine Klassendeklaration schreibt man hier eben Interface
statt Klasse, dann den Namen des Interfaces, wie Point im Beispiel.
Dann kann man, haben wir gesagt, eigentlich nur Methoden können hier
vorkommen, aber man kann auch Konstanten noch dazunehmen in den
Interface, also Konstanten und Methoden, aber keine Attribute.
Dann für die Implementierung gibt es in dieser Schüsselwort
Implements.
Also wenn Sie eine Klasse haben, die so eine Schnittstelle
implementiert, also bestimmte Methoden eben, dann wird dazu dieses
Implements Schlüsselwort verwendet.
Was auch geht, ist, dass eine Klasse mehrere Interfaces gleichzeitig
implementiert.
Das geht beim Beerben ja nicht, also Sie können nicht hinter Extents
mehrere Klassen angeben, da ist immer nur eine Klasse möglich.
Bei Implements können Sie mehrere Interfaces angeben, also eine Klasse
kann mehrere Interfaces gleichzeitig implementieren.
Hier können mehrere Interface-Namen stehen, die Sie mit Kommas
abtrennen.
Und wenn Sie mehrere angeben, bedeutet das, dass Ihre Klasse sämtliche
Methoden, die in allen diesen Interfaces angegeben sind,
implementieren.
Wenn es da Überschneidungen geben sollte, dass in zwei Interfaces
dieselben Methoden verlangt werden, dann reicht es, wenn Sie die
einmal implementieren, Sie können ja auch gar nichts anderes machen,
also man kann ja nicht zwei Implementierungen da direkt angeben.
Und wie gesagt, jede Methode muss einmal implementiert sein.
Also schauen wir uns mal nochmal an, wie das verwendet werden kann, an
der Hand von unserer Punktklasse.
Also wenn Sie jetzt zum Beispiel so eine Methode haben wollen, die
macht jetzt nichts besonders sinnvoll, die soll bloß diesen Punkt
verschieben, um 3 in x-Richtung und 5 in y-Richtung.
Und weil diese Methode eben unabhängig von einer konkreten
Implementierung ist, reicht es aus, wenn Sie eben hier dieses
Interface Point verlangen.
Das heißt, egal wie die Implementierung aussieht, alles was eben
dieses Point Interface erfüllt, passt hier als Parameter für diese
Klasse.
Das ist schon mal eine schöne Sache, weil man kann dann eben diesen
Code sehr allgemein schreiben.
Also dieser Code, den Sie hier geschrieben haben, der funktioniert
eben für alles, was dieses Interface Point implementiert.
Egal wie das eben dann wirklich implementiert ist.
Sie wissen, diese Methode gibt es, die muss irgendwie in den Klassen,
die das Interface implementieren, vorhanden sein.
Also können Sie das auch vorher verwenden.
Okay, schauen wir uns nochmal ein Beispiel an.
Also was passiert, wenn wir jetzt sowas hier hinschreiben.
Zum Beispiel haben wir hier PolarPoint, haben wir ja vorher gesehen,
das implementiert dieses Interface Point.
Also legen wir so einen neuen Punkt hier an mit 90, ich weiß nicht
mehr, die Reihenfolge 90° sieht eher nach Winkel aus, sind das im
Radius.
Rufen dann die Methode foo auf, legen noch so einen zweiten Punkt an,
rufen wieder die Methode foo auf.
Also das Schöne ist mal, dass die Methode foo eben mit beiden Objekten
da kommt, weil die beide das Interface implementieren.
Hier oben ist es so, dieses P hier oben hat einen statischen Typ Point
und ähnlich wie bei der dynamischen Bindung wird dann eben jetzt auch
hier zur Laufzeit ermittelt, was steckt denn da wirklich dahinter und
dann die entsprechende Methode aufgerufen.
Also wenn wir hier dieses Q reinstecken, dann hat es eben den
dynamischen Typ PolarPoint und hier bei Shift wird dann die
entsprechende Methode PolarPoint aufgerufen.
Also dynamische Bindung passiert hier genauso, wie Sie es bei
Verarbung vorher gesehen hatten.
Also wird hier oben eben die entsprechende Methode Shift auf der
Klasse, also hier, wenn man hier foo aufruft, wird hier Shift aus der
Klasse PolarPoint aufgerufen.
Hier unten, wenn Sie so einen Cartesian Point haben und das an die
Methode foo übergeben, wird eben hier oben auch mit dynamischer
Bindung dann wieder festgestellt, das ist aus der Klasse Cartesian
Point und da müssen Sie eben die Shift-Methode aufrufen.
Also bei foo Q wird Shift aus PolarPoint aufgerufen und bei foo R wird
Shift aus der Klasse Cartesian Point aufgerufen.
Also auch dynamische Bindung hier.
Ok, schauen wir uns nochmal ein Beispiel an, und zwar schauen wir uns
jetzt noch Listen an.
Nehmen wir an, Sie haben einen Algorithmus, der soll auf einer Liste
von Punkten arbeiten und der soll möglichst unabhängig von der, auch
noch zusätzlich, also nicht nur von der Implementierung der Punkte
unabhängig sein, sondern auch von der Implementierung der Liste
unabhängig sein.
Also wie können wir das machen?
Wir können wieder ein Interface implementieren für diese Liste.
Also wir können so ein Interface Point List machen, das von der
Implementierung der einzelnen Punkte in der Liste abstrahiert.
Also ein Interface Point List kann so aussehen, dass wir hinschreiben,
ja es gibt eben hier in dieser Liste die Methoden, um was einzufügen,
am Anfang und am Ende, und um am Anfang und am Ende der Liste die
entsprechenden Einträge abzufragen und noch eine ganze Reihe von
anderen Methoden.
Und hier was verwendet wird, oder was in diese Liste reingesteckt
wird, ist einfach nur irgendein Objekt, was dieses Point Interface
implementiert.
Das reicht uns an der Stelle aus.
Wir müssen hier nichts wissen über die konkrete Implementierung.
Den Listen ist es relativ egal, was sie da an konkreten Objekten drin
stehen haben, aber es sollen sich eben um Punkte handeln.
Das können wir durch dieses Point hier, das wir eben hier angeben.
P ist was, was dieses Interface Point implementiert, angeben dann.
Und wenn wir dann eine konkrete Liste haben, also wenn wir zum
Beispiel so eine doppeltverkettete Punktliste haben, dann könnte man
eben auch wieder angeben, dass diese Klasse das Pointlist Interface
implementiert und das macht die dann eben auf die entsprechenden, so
wie wir es schon mit der Listenvorlesung gesehen hatten, passiert das
dann.
Aber was schön ist, wir können durch dieses Interface abstrahieren von
der konkreten Listenimplementierung, also ob das jetzt eine
doppeltverkettete Liste ist oder nicht, ist es dann egal, sondern man
sieht nach außen hin praktisch immer nur diese Pointlistes, was
abstrakter ist eben.
Okay, also was ist ein Interface?
Reine Definition von Typen.
Also wir haben nur die Signaturen angegeben, definiert dadurch eine
Schnittstelle.
Nichts zur Implementierung hat gesagt, keine Attribute sind angegeben.
Und die eigentliche Implementierung von einem Interface, die findet
dann in abgeleiteten Klassen statt oder in Klassen, die halt dieses
Interface implementieren, wird durch dieses Implementschlüsselwort
angezeigt.
Objekte können mehrere Typen haben.
Also es ergibt sich daraus, dass man eben mehrere Interfaces angeben
kann und was man noch zusätzlich machen kann, ist eben, dass man diese
Klasse von einer anderen Klasse ableitet und zusätzlich Interfaces
hat.
Also Sie können Extents und Implements kombinieren.
Bei Extents-Ableitung ist immer eine Klasse, von der das abgeleitet
wird, Interfaces können es mehrere sein und dadurch kann es eben auch
vorkommen, dass eine Klasse jetzt mehrere Typen hat.
Eben die Interface-Typen plus den Typ, der durch die Klassen
-Hierarchie und durch die Verarbeitung entsteht.
Bei Interfaces ist es ähnlich wie bei den Absagenklassen, Sie können
keine Objekte anlegen.
Sie können nicht irgendwie New aufrufen jetzt.
Das ist ja auch klar, weil eben für die Implementierung nichts bekannt
ist.
Das heißt, nur Schnittstelle, das reicht nicht aus, um damit Objekte
anlegen zu können.
Sondern das müssen dann wirklich die konkreten Objekte sein, für
solche Dinge anlegen.
Also für die konkret implementierten Klassen.
Gut, zum Abschluss jetzt nochmal eine Frage.
Gehen wir nochmal auf das andere System hier, schalten wir nochmal um.
Da ist schon die Antwort bekannt, genau gegeben worden.
Schauen wir mal kurz, also was war die Frage hier?
Welche Aussage zu Interfaces und Abstraktenklassen ist nicht richtig?
Also man kann eine abstrakte Klasse von einer anderen abstrakten
Klasse ableiten, eine von einer abstrakten Klasse ableitete Klasse
muss alle abstrakten Methoden implementieren.
Das sind relativ komplizierte Sachen, eine Klasse kann mehrere
Interfaces implementieren, man kann ein Interface von einem anderen
mittels Extents ableiten.
In einer abstrakten Klasse können sämtliche Methoden nicht abstrakt
sein.
Also welche eine davon trifft nicht zu?
Muss ich selber auch nochmal schauen.
Welches Ergebnis haben wir hier?
Ah, das ist noch relativ gemischt.
Es gibt irgendwie für D und E Gleichstand, die liegen vorne, jetzt
liegt E langsam vorbei.
Gehen wir von oben her durch.
Man kann eine abstrakte Klasse von einer anderen abstrakten Klasse
ableiten.
Das geht, das können Sie machen.
Genauso eine von einer abstrakten Klasse ableitete Klasse muss alle
abstrakten Methoden implementieren.
Ist das der Fall?
Nein, das höre ich hier schon aus dem Publikum.
Das ist richtig, also eine abstrakte Klasse hat ja eben manche nicht
implementierte Methoden und wenn Sie davon ableiten, kann wieder eine
abstrakte Klasse entstehen, in der immer noch bestimmte
Implementierungen eben offengelassen sind.
Und Sie müssten dann in einem weiteren Schritt nochmal davon ableiten,
um eine konkrete, instanziierbare Klasse, von der Sie auch Objekte
anlegen können, zu erhalten.
Also ist B das, was hier nicht zutrifft.
Und die anderen Sachen, die gelten alle, also Klasse kann mehrere
Interfaces implementieren.
Klar, das hatten wir gesehen.
Ein Interface von einem anderen ableiten hatten wir auch gesehen.
Und in einer abstrakten Klasse können sämtliche Methoden nicht
abstrakt sein.
Das kann man machen, dann ist abstrakte Klasse vielleicht nicht so
sonderlich sinnvoll.
Dann könnte man das Schlüsselwort auch weglassen, aber das geht auch.
Gut, dann mit dieser Frage beenden wir die Vorlesung für heute und wir
sehen uns nächste Woche nochmal, auch wenn es ganz kurz vor
Weihnachten ist.
Bis dahin.