Zwei Themen, die wir uns anschauen werden.
Für das erste sehen Sie hier schon die Titelfolie.
Das nennt sich Vom Programm zur Maschine.
Also dort werden wir uns etwas genauer anschauen, wie Java übersetzt
wird in Maschinencode.
Das wird uns ungefähr die erste halbe Stunde beschäftigen.
Danach haben wir noch ein zweites Thema.
Das nennt sich Best Practices.
Da geht es vor allen Dingen darum, wie man Vergleichsoperationen in
Java sinnvoll implementiert.
Gleich am Anfang möchte ich einen Literaturhinweis geben, aus dem auch
viele Inhalte der heutigen Sitzung stammen.
Und zwar ist das ein Buch von Prof. Goos, bei dem ich damals, vor 15
Jahren ist es mittlerweile, das Vergnügen hatte, Informatik 1 in
diesem Hörsaal hier hören zu dürfen.
Das war ganz neu.
Prof. Goos ist mittlerweile emeritiert, aber das Buch hier ist eine
mehrbändige Ausgabe.
Es sind vier Bände Vorlesungen über Informatik.
Das ist ein Klassiker und auch aus dem kommen die Inhalte der heutigen
Vorlesungen eben aus dem Band 2.
Und dort das Kapitel 11 ist auch online verfügbar im Springer Link
Portal.
Wenn Sie also hier im Uninetz sind, können Sie sich das auch online
anschauen.
Ja, die Lernziele für heute.
Sie sollen also verstehen, wie die Halde auf Englisch heap und der
Kellerspeicher auf Englisch stack funktioniert.
Das kennen Sie vielleicht schon aus Grundbegriffe der Informatik oder
aus anderen Veranstaltungen.
Aber hier gucken wir uns jetzt vor allen Dingen an, was dann halt auch
für Java relevant ist für Sie.
Sie sollen also diesen Einsatz der Datenstrukturen erläutern können.
Das ist der Sinn.
Also natürlich programmieren Sie jetzt hier später keinen Compiler.
Das ist nicht Gegenstand dieser Vorlesung, aber Sie sollen eben die
Wirkungsweise verstanden haben und diese Datenstrukturen erläutern
können.
Ebenso sollen Sie auch dann erklären können, wie eben so Sprünge im
Programmcode funktionieren.
Ja, und letztendlich, was wir dann heute uns als Letztes auch noch
angucken werden und dazu gehören die Lernziele.
Sie sollen also die Funktionsweise und den Einsatz dieser
Datenstrukturen dann auch anhand von solchen Methoden aufrufen,
erläutern können und auch erklären, was beim Verlassen von einer
Methode passiert.
Gut, damit möchte ich dann noch einen Überblick geben über das, was
heute drankommt.
Orientiert sich natürlich an den Lernzielen.
Also zuerst gucken wir uns Halde- und Kellerspeicher an.
Dann zweitens gucken wir uns an, was passiert mit diesen
Datenstrukturen, wenn eine Methode aufgerufen wird.
Wie ist der Speicher eingeteilt?
Wie wird Speicher adressiert?
Und dann vor allen Dingen, was passiert bei Sprüngen?
Das ist ja da das Interessante.
Und dann natürlich die letzten beiden Punkte.
Das, was ich gerade schon gesagt habe, Methodenaufruf und was
passiert, wenn wir eine Methode verlassen.
Damit komme ich dann zur Motivation.
Nochmal so ganz grundlegend.
Sie wissen, wie das funktioniert, wenn Sie Ihren Java-Programm
ausführen.
Sie schreiben also erstmal Java-Code.
Das ist hier.
Das ist diese Java-Datei.
Dann haben Sie einen Compiler, der Ihnen das übersetzt in Bytecode.
Und letztendlich haben Sie dann auf Ihrer Maschine eine Java-Virtual
-Maschine laufen, die das Ganze dann ausführt.
Und die ist dann tatsächlich plattformabhängig.
Also wenn Sie die auf Ihrem Desktop-Rechner ausführen, dann übersetzt
Ihnen diese JVM eben diesen Java-Bytecode in x86-Code.
Wenn Sie es auf Ihrem Telefon ausführen, ist da wahrscheinlich ein ARM
-Prozessor drin.
Dann würde die JVM, zum Beispiel die JVM von Android, würde das so
übersetzen, dass da eben der richtige Maschinen-Code dabei rauskommt.
Und das ist ein großer Vorteil für Sie in Java, dass Sie quasi auf
verschiedenen Prozessor-Architekturen dieselbe Programmiersprache
verwenden können, weil dort eben diese Java-Virtual-Machine dazwischen
ist und Ihnen da eine plattformunabhängige Schnittstelle sozusagen
bietet.
Ja, aber auch dort natürlich wird dann auf der Hardware irgendwann mal
Speicher adressiert und da gibt es jetzt eben diese beiden
Datenstrukturen Halde und Keller, die ich Ihnen heute vorstellen werde
und die da eine Rolle spielen.
Die Begrifflichkeiten...
Ja, also man kennt meistens die englische Ausdrücke hier heap.
Das ist also auf Deutsch die Halde.
Das hat damit zu tun, wie das organisiert ist und das ist ein
reservierter Speicher, wo eben dann zur Laufzeit nach Bedarf Elemente
reingespeichert werden können.
Das kann eine beliebige Anordnung haben, das hängt dann davon ab, eben
wie die Implementierung Ihrer virtuellen Maschine zum Beispiel ist,
wie die diesen Speicher zuweist und freigibt.
Und wenn etwas referenziert wird, wird dann eben Speicher zugewiesen
und wenn dieses Referenzieren aufgehoben wird, dann wird der Speicher
wieder freigegeben.
In Java zum Beispiel werden dort in dieser Halde die Objekte
gespeichert.
Also jedes Mal, wenn Sie in Java ein Objekt instanziieren, wird dort
eben ein Speicherbereich dafür dann benutzt.
Und ja, wir sehen hier, wie diese Halde aufgebaut ist.
Es gibt halt eine Speicheradresse, die nennt sich minHeap und es gibt
eine, die heißt maxAddress hier, die zeigt also, welche Adressen Sie
im Speicher benutzen können, um diese Halde anzusprechen.
Das ist also die eine Datenstruktur.
Wir sehen dann gleich, wie das tatsächlich funktioniert, wenn die
verwendet wird.
Die andere ist der Kellerspeicher, auf Englisch stack.
Das weiß ich auch noch gut, wie Professor Goos das damals erklärt hat.
Eigentlich ist der Originalbegriff der Deutschen, der Keller.
Das wurde dann ins Englische übersetzt stack und irgendwann wieder
zurückübersetzt, weil stack heißt ja nicht nur Keller, das heißt auch
Stapel.
Deswegen liest man im Deutschen manchmal auch Stapelspeicher.
Wir benutzen hier den klassischen Begriff Kellerspeicher, sozusagen
das Original, der wird jetzt auch im Rest der Vorlesung dann heute
benutzt werden.
Die Arbeitsweise von so einem Kellerspeicher ist last in, first out.
Das heißt also, die Elemente, die Sie als letztes draufgelegt haben,
und deswegen ist vielleicht ja diese Analogie vom Keller ganz gut.
Sie können sich das vorstellen wie so eine Kartoffelstiegene, die Sie
als letztes oben draufgelegt haben, würden Sie als letztes oben wieder
rausnehmen, außer Sie haben unten so eine schöne Schütte.
Aber jedenfalls stellen Sie es sich vor wie Sprudelkästen, die Sie
aufeinander stellen.
Sie können halt nur den obersten runternehmen im Keller.
Und wenn man dort jetzt quasi so einen Sprudelkasten hinstellt, nennt
sich das Ganze Push-Operation.
Da wird also etwas in den Keller gelegt und die gegenteilige Operation
runternehmen nennt sich Pop.
Ja, die Befehlsreihenfolge habe ich hier in dem Beispiel mal gezeigt.
Angenommen, wir machen jetzt Push auf einem Element A, dann steht das
eben drin, dann machen wir Push B, dann kommt es drauf, Push C, Push D
und dann haben wir hier so schön aufgestapelt eben unseren
Kellerspeicher.
Wichtig ist, dass wir jetzt nicht einfach ein Element aus der Mitte
rausnehmen können, sondern wenn wir jetzt auf ein Element zugreifen
wollen, ist das nur für das oberste möglich.
Also wenn wir jetzt Pop ausführen auf unserem Kellerspeicher, kriegen
wir auch das Element D zurück.
Das ist also die Rückgabe dieses Pop-Aufrufs und das wird dann
dementsprechend gelöscht aus dem Kellerspeicher.
Dann kann man eben wieder Elemente runternehmen und könnte auch wieder
neue drauflegen.
Aber wichtig ist, da ist die Reihenfolge dann quasi festgelegt.
Sie können immer nur das letzte, was oben drauf ist, wieder
runternehmen.
Was Sie gerade schon gesehen haben, so ein Kellerspeicher, der kann
also dynamisch wachsen und schrumpfen, je nachdem, wie viel Sie
drauflegen.
So hoch ist er halt.
Und auch bei dem Kellerspeicher ist es so, dass Sie den zur Laufzeit
allozieren.
Das wird also nicht irgendwie vorher schon festgelegt, sondern
tatsächlich erst zur Laufzeit.
Und ganz genau ist es auch so, wenn der Code, der darauf liegt, auf
diesen allozierten Speicher bzw.
darauf zugreift, wenn der eben fertig ist, dann wird dieser Speicher
auch wieder freigegeben.
Ganz häufig benutzt man das bei Prozessoren.
Zum Beispiel, wenn dort Unterprogramme ausgeführt werden, wird auf so
einem Keller speichert, die Rücksprungadresse gespeichert.
Auch die Rückgabewerte von so einem Unterprogramm werden dort
gespeichert.
Und das sehen wir dann nachher auch, wie das bei so einem
Methodenaufruf aussieht.
Dritter Punkt hier.
Also es werden auch alle Parameter der aktuell laufenden Programme
drauf gespeichert.
Zum Beispiel, wenn Sie jetzt einen geschachtelten Aufruf haben.
Sie haben eine Rekursion, eine Methode, ruft sich selbst auf, dann
wird das eben darauf gespeichert, auf diesem Kellerspeicher.
Auch der Aufruf von Unterprogrammen und letztendlich die lokalen
Variablen, die es gibt, die werden alle auf diesem Kellerspeicher auch
gespeichert.
Ich habe schon gesagt, er kann wachsen und schrumpfen.
Und damit man weiß, wie groß er ist, dafür gibt es eben so einen
Pegel.
Der zeigt halt eben an, wie jetzt hier in diesem Beispiel, da der
Kellerspeicher wächst und schrumpft.
Ist jetzt wieder ein bisschen komisch hier in der Darstellung.
Eigentlich habe ich ja vorhin gesagt, unten stehen die Dinge und ich
tue sie oben drauf.
In dieser Grafik jetzt hier wird tatsächlich von oben nach unten
gerechnet.
Also da wächst der Keller nach unten und das ist auch im Folgenden so.
Ist jetzt ein bisschen seltsam, aber wenn Sie mal einen Forstwirt
erzählen, was wir Informatiker unter Bäumen verstehen, wo die Wurzel
oben ist und die Blätter unten sind, das ist eigentlich auch auf dem
Kopf.
Aber man hat sich dran gewöhnt und ich hoffe, Sie gewöhnen sich auch
an diese Darstellung hier.
Genau, dann habe ich schon gesagt, es gibt eben so einen Adressraum,
der hat gewisse Grenzen.
Also hier zum Beispiel 0 und Max Address gibt also an, wo hier die
maximale Adresse ist.
Und innerhalb von so einem Speicher können Sie dann natürlich mit
Hilfe dieser Adresse verschiedene Speicherzellen ansprechen und die
zugreifen.
Und ja, da ist dann natürlich ein gewisser Inhalt drin.
Und was kann dieser Inhalt sein?
Tatsächlich die Befehle, die also ein Programm enthält.
Das ist hier dieser oberste Teil, das ist der Speicher für die
Befehle, Programmbereich.
Dann haben Sie auch Konstanten.
Entschuldigung, links Druckfehler.
Natürlich soll es Konstanten heißen.
Da sind die ganzen Literale des Programms drin gespeichert, die sich
nicht mehr ändern zur Ausführung.
Und hier bei den Daten haben Sie dann tatsächlich alles, was variabel
ist.
Das wird dann hier drin gespeichert.
Ja, warum ist das so?
Sie wissen ja vorher nicht, wie viele Referenzobjekte Sie haben.
Sie wissen dynamisch nicht, wie viel in jeder Ausführung des Programms
angelegt wird.
Und deswegen haben Sie eben so einen Datenbereich, der nach unten
wachsen kann.
Bei den Konstanten, da wissen Sie es vorher, die stehen ja schon im
Programmcode drin.
Die sind festgelegt und können zur Laufzeit nicht mehr geändert
werden.
Aber bei den Variablen ist es natürlich anders.
Dieser Datenbereich, der wird dann nochmal aufgeteilt.
Da gibt es dann wieder verschiedene Grenzen.
Hier MINVAR zeigt eben an, wo die Programmvariablen beginnen.
Und das sind eben dann dynamische Variablen mit unbeschränkter
Lebensdauer.
Die sind in diesem Teil des Speichers drin.
Und ganz unten ist dann eine Halde, wo anonyme Objekte sind.
Da weiß man nicht, wie lange die leben.
Also von den oben weiß man, die sind unbeschränkt gültig.
Und bei den unteren kann es sein, die werden mal wieder irgendwann
freigegeben.
Gut, dann kommen wir als nächstes zu den Sprüngen.
Hier geht es also um den Kontrollfluss des Programms.
Was passiert, wenn Sie jetzt solche Verzweigungen angeben, die Sie
halt in Java jetzt wahrscheinlich schon relativ intuitiv verwenden.
Also if-then-else, wenn Sie schleifen mit while oder mit for oder so
weiter machen.
Wie funktioniert das denn tatsächlich auf der Maschine?
Wie wird es im Maschinencode ausgeführt?
Ja, dafür gibt es eine Variable, die nennt sich Befehlszähler.
Auf Deutsch haben wir das jetzt hier auch mit bz dann dementsprechend
abgekürzt.
Die gehört nicht zum Speicher und die enthält während der Ausführung
eben die Adresse des nächsten auszuführenden Befehls.
Damit man also die Maschine beim Ausführen weiß, in welche Adresse
muss ich als nächstes reinschauen, um zu wissen, was ich eigentlich
ausführen muss.
Ja, und dieser Wert wird, wenn man ganz normal ein Programm von oben
nach unten ausführt, also sequenziell wird die einfach nach jeder
Ausführung um eins erhöht und dann wird eben in die nächste
Speicherzelle reingeschaut, was da drinne steht.
Genau, und falls das Programm mit einer Ausführung von b fortgesetzt
wird, dann kann das sein, dass eben diese Adresse b drin steht mit so
einem Markierer.
Den kann man dann auch benennen, damit es einfacher zu lesen ist.
Und das ist vielleicht das, was diejenigen von Ihnen, die noch ganz
früher mit Basic mal programmiert haben, noch als Go-To-Befehl kennen.
Dort kann man eben so eine Sprungmarke quasi definieren mit M,
deswegen heißt die auch so und kann dann so eine Zuweisung an den
Befehlszähler machen, dass er eben dorthin springen soll.
Und dann ist das M das Sprungziel, wo dann die Ausführung weitergeht.
Ja, wir haben dann im Folgenden jetzt mal ein Beispiel aufgeführt, wo
drei Formen von Sprünge drin vorkommen.
Das Erste sind unbedingte Sprünge.
Also wenn Sie hinschreiben GoToM, dann geht das Programm dorthin.
Und das heißt dann in dem Fall, wenn Sie den Befehlszähler mit I
entweder erhöhen oder verringern, also hier BZ plus minus I, heißt der
GoTo -Befehl eben, dass der Befehlszähler neu gesetzt wird auf dieser
Adresse, wo das M steht.
Also das ist unbedingt, da haben wir früher dann im Gymnasium im
Unterricht immer schöne Sachen mitgemacht mit Basic, BMW 10, Printout,
der Lehrer ist blöd, 20, GoTo10.
Da können Sie sich überlegen, was dann passiert ist.
Und es gibt natürlich dann auch kompliziertere Sprünge.
Es gibt If-Then-Abfragen, wo eben bedingte Sprünge passieren, je
nachdem, ob ein gewisser Vergleich nach wahr auswertet oder nicht.
Und Sie können auch Interprogramm-Sprünge machen.
Also das Ganze nennt sich dann GoSub, da können Sie quasi
Unterprogramme ausführen.
Und das ist dann ungefähr so, wie wenn Sie eine Methode ausführen, die
keinen Parameter hat.
Sie schreiben einfach irgendeine Methode hin, ohne irgendein
Parameter, ist das gleiche, wie wenn Sie mit GoSub einfach dorthin
springen.
Genau, und dann ist natürlich wichtig, wenn Sie in so ein
Unterprogramm reinspringen, muss ja die Maschine irgendwie wissen,
wenn das beendet ist, wo geht es dann wieder weiter, der
Kontrollfluss, nachdem dieses Unterprogramm abgearbeitet wird.
Also wird nicht nur der Befehlszähler auf dieses Sprungziel n gesetzt,
sondern es wird auch eine Rückkehradresse definiert.
Die nennt sich dann bzalt in dem Fall und wird dann auch hochgezählt.
Man kann auch relative Adressen angeben, also wenn Sie zum Beispiel an
einer Adresse m' gerade sind, dort steht ein Befehl drin und dieser
Befehl heißt GoToM und man kann eine relative Adresse eben als Abstand
von m und m' berechnen.
Dann ist dieses GoToM einfach GoToBefehlszähler plus i.
Also das ist eigentlich das, was oben steht.
Jeder, wo gerade Laserpointer auch ist, ist gerade das gleiche
nochmal.
Genau, dann, das hier ist jetzt tatsächlich Wiederholung aus GBI.
Dort wollte ich Ihnen jetzt nochmal kurz zeigen, wie tatsächlich
solche Sprünge, die Sie in Java hinschreiben, umgesetzt werden in die
Assembler -Sprache.
Also nochmal zur ganz kurzen Erinnerung.
Assembler, hier unten links, habe ich so ein paar Befehle nochmal
aufgeführt.
Also ldv heißt eben, ich lade etwas in den Akkumulator rein.
Dieses eql vergleicht den Inhalt von dem Akkumulator mit dem, was
danach steht.
not negiert dann den Inhalt und jmn ist ein bedingter Sprung.
Also was passiert, wenn ich hier so ein if habe, ich vergleiche
irgendwie a1 mit a2, dann heißt es in Assembler, naja, ich lade
erstmal das a1, dann vergleiche ich es, ob a1 gleich a2 ist.
Dann möchte ich ja, also wenn es wahr ist, möchte ich das eben nicht
machen, deswegen negiere ich das hier.
Also wenn es wahr ist, springe ich nicht, aber wenn es halt falsch
ist, springe ich bis zum Ende, weil ich ja nur im Falle, dass die
Bedingung wahr ist, das s ausführen möchte.
Und wenn diese Bedingung dann wahr ist, dann wird weitergegangen und
das s wird auf jeden Fall ausgeführt.
Wenn also diese negierte Bedingung zutrifft, dann wird eben ans Ende
gesprungen und das ist dann hier diese Sprungmarke.
Jump n ist eben ein bedingter Sprung, der dann guckt, was im
Akkumulator drin steht, wahr oder falsch und springt in diesem Fall
dann weiter.
Ja, das ist jetzt natürlich ein bisschen einfach, weil wir nur einen
if -Fall haben.
Wenn noch ein else-Fall ist, dann müssen wir schon noch eine
zusätzliche Sprungbedingung einführen.
Hier unten, das ist das Jump, das ist der unbedingte Sprung.
Also der springt immer, egal, was im Akkumulator drin steht.
Und dann können Sie es wieder genauso machen.
Sie laden also diese zwei Variablen an.
Sie laden die erste in den Akkumulator, vergleichen und dann gibt es
eben diesen bedingten Sprung.
Wenn das zutrifft, springen Sie auf das den.
Wenn nicht, dann wird hier das else ausgeführt.
Aber es kommt ein unbedingter Sprung, es wird gleich zum Ende
gesprungen und dann wird u auf jeden Fall ausgeführt.
Also mit diesen primitiven Anweisungen können Sie dann alles das
machen, was Sie in Java eigentlich kennen.
Sie haben diese if-then-else, Sie haben Schleifen, wie wir jetzt
gleich sehen.
Das wird alles im Prinzip umgesetzt auf diese Assemblerbefehle und das
macht eben die virtuelle Maschine für Sie.
Und Sie können dann diese hochsprachlicheren Konstrukte hinschreiben.
Als letztes hier die while-Bedingung.
Dort passiert eben was ganz ähnliches.
Wir laden wieder, vergleichen.
Das ist noch relativ einfach.
Dann wird hier wieder negiert und falls die Schleifenbedingung nicht
mehr zutrifft, springt man ganz zum Ende.
Das ist also ein bedingter Sprung.
Wenn sie doch zutrifft, führt man das else aus und springt wieder
zurück zur Marke while.
Also wieder am Anfang.
Sie sehen, dann geht halt immer diese Schleife hin und her, bis
irgendwann die Bedingung nicht mehr zutrifft und dann springt sie zum
Ende.
Und damit haben Sie eine while-Schleife eben mit diesen Sprüngen
implementiert.
So hat man das früher tatsächlich gemacht in den Programmiersprachen,
wo sowas wie while noch nicht zur Verfügung stand.
Und ja, seien Sie froh, dass Sie das jetzt nicht mehr machen müssen.
Und eben in Java und in eigentlich allen anderen modernen
Programmiersprachen, da diese bequemen Konstrukte verwenden können.
Genau, dann gucken wir uns nochmal an, wie das Ganze funktioniert mit
den Adressen jetzt.
Also Sie haben, wie gesagt, diese minvar-Adresse, die sagt Ihnen, wo
der Speicherbereich mit diesen Programmvariablen anfängt.
Und dann haben Sie auch einen Umgebungszeiger.
Der sagt Ihnen dann, wo das laufende Unterprogramm, was Sie gerade im
Moment ausführen, wo das gerade ist und wo die dynamischen Variablen
von dem sind.
Dann der Pegel hier, der sagt, wo das oberste Element im
Laufzeitkeller ist und ist dann damit quasi die Basisadresse für die
Zwischenergebnisse.
Und jedes Mal, wenn ein Unterprogramm aufgerufen wird, wird er um 1
erhöht.
Und so können Sie sich dann auch die Adressen ausrechnen von
verschiedenen Variablen.
Also wenn Sie eine Variable v haben, die so eine relative Adresse rv
hat, dann ist die hier oben in diesem Bereich hier drin.
Und Sie können dann damit einfach mit dieser Addition hier unten
addieren, wo da die Adresse ist, wo Sie hin müssen.
Und bei einer dynamischen Variable, dementsprechend gucken Sie, wo
gerade dieser Umgebungszeiger steht und können es dann auch
ausrechnen.
Ja, der Laufzeitkeller, habe ich vorhin schon gesagt, der speichert
die dynamischen Programmvariablen.
Und wir brauchen eben einen Keller, damit der wachseln kann, wenn wir
geschachtelte Unterprogrammaufrufe haben.
Wir haben gerade eben auf der Folie vorher schon den Begriff Schachtel
gehört.
Also das habe ich hier nochmal hingeschrieben.
Eine Schachtel nennt sich auf Englisch auch stack frame oder
activation record.
Das ist quasi so eine Einheit, in die der Kellerspeicher aufgeteilt
ist.
In der untersten Schachtel stehen die statischen Variablen drin.
Und ansonsten enthält so eine Schachtel lokale Variablen,
Rückkehradressen und weiteres.
Hier auf der nächsten Folie habe ich Ihnen das mal dargestellt, wie
das dann tatsächlich aussieht, so eine Schachtel.
Also hier sind zwei Programme.
Es ist ein aufrufendes Programm.
Bei dem stehen die Parameter drin, die Rückkehradresse und die lokalen
Variablen.
Und wenn dann eben ein Unterprogramm aufgerufen wird, hat das wieder
eigene Variablen und eigene Parameter, aber auch diese
Rückkehradresse.
Und die zeigt eben in das aufrufende Programm, damit man weiß, wo der
Kontrollfluss weitergeht.
Und insgesamt, der Pegel des Kellers wird durch die Gesamtheit dieser
Schachteln dann ausgedrückt.
Was passiert also, wenn man so eine Methode aufruft?
Also Sie haben jetzt ein Programm, da wird eine Untermethode
aufgerufen.
Dann müssen Sie natürlich den Kellerpegel erhöhen, weil jetzt kommt
eine neue Schachtel rein.
Dann weisen Sie da diese Werte zu, der Parameter.
Sie tragen auch die Rückkehradresse ein und setzen dann den
Umgebungszeiger dann dementsprechend hier auf die neue Schachtel.
Und dann wird hier unten die Methode aufgerufen.
Es springt halt hier auf die erste Adresse rein und nach dem Aufruf
geht es dann wieder zurück an die Rückkehradresse.
Das ist dann auch das, was auf der nächsten Folie kommt.
Was machen wir also, wenn wir jetzt so ein Unterprogramm aufgerufen
haben?
Es wird irgendwie ein Ergebnis vielleicht zugewiesen.
Das Unterprogramm soll ja irgendwas Sinnvolles machen.
Und diese Rückkehradresse, die vorher gespeichert wurde, wird an einen
Register zugewiesen.
Der Umgebungszeiger wird neu gesetzt.
Und der Kellerpegel wird wieder erniedrigt, weil das Unterprogramm ist
fertig unten.
Das brauchen wir nicht mehr.
Und dann gibt es einen unbedingten Sprung zurück in die
Rückkehradresse, weil das Programm ja fertig ist.
Und egal, wie das Ergebnis war, das Programm muss ja dann irgendwann
sich beenden.
Und dann wird der Kellerpegel wieder zurückgesetzt.
Und dann kann der wieder für was anderes genutzt werden.
Also diese Schachteln hier, das müssen Sie sich immer so ein bisschen
vorstellen.
Das sind eben diese Speicherbereiche, auf denen dann so ein
Unterprogramm, was aufgerufen wird, arbeiten kann.
Und das Ganze hier sind also Adressen in Ihrem Speicher, die dann
dementsprechend dynamisch zugewiesen und wieder freigegeben werden.
Das also, wie gesagt, ein kleiner Exkurs in die Maschinenwelt.
Das ist ja jetzt hier in der Programmieren-Vorlesung nicht das
Hauptbetrachtungsziel, eigentlich sich anzugucken, wie sowas auf
Maschinencode ausgeführt wird.
Aber es ist natürlich etwas, was auch Teil der Informatik ist und was
dann in weiteren Vorlesungen noch vertieft wird.
Ja, was haben wir jetzt in diesem ersten Teil für heute, in der ersten
halben Stunde gesehen?
Also es gibt zwei verschiedene Datenstrukturen, die Halde- und den
Keller speichern.
Die Halde, also merken Sie den englischen Begriff Heap, weil den liest
man viel häufiger.
Wenn Sie eine Fehlermeldung in Java haben, wenn irgendwie Ihr Programm
sich beendet wegen einem komischen Fehler, dann steht dort öfter
natürlich drin, dass da, keine Ahnung, ein Stack Overflow oder Heap
Error oder sonst irgendwas passiert ist.
Dann wissen Sie, was das heißt.
Also die Halde ist ein reservierter Speicher und dort werden zur
Laufzeit in beliebiger Reihenfolge dann Elemente drin gespeichert.
Im Keller Speicher ist die Reihenfolge nicht beliebig.
Da haben Sie dieses Last-in-First-out-Prinzip und das wird dann eben
immer, wie Sie das gerade gesehen haben, bei diesem Programm aufrufen,
eben draufgestapelt und wieder runtergenommen.
Und wenn so ein Code abgearbeitet ist, wird der Speicher auch wieder
freigegeben.
Sowas benutzen auch Prozessoren dann intern, aber das benutzt eben
dann auch Java als Datenstruktur in der virtuellen Maschine.
Ja, woraus besteht dann so ein Speicher von einem Programm?
Sie haben tatsächlich die Befehle irgendwo drin stehen.
Sie haben einen konstanten Bereich für die ganzen Literale, von denen
Sie vorher schon wissen, wie groß die sind und wie die belegt sind.
Und dann haben Sie für die Variablen einen Datenbereich, der wachsen
und schrumpfen kann.
Was Sie sich auch merken können, sind eben diese drei Formen von
Sprünge.
Wir haben also diesen unbedingten Sprung mit GoTo, dann einen
bedingten Sprung und noch diese GoSub-Aufrufe, die dann in
Interprogrammsprung genannt werden, bei der quasi Unterprogramme dann
aufgerufen werden oder halt in den Programmen hin- und hergesprungen
werden.
Deswegen der Name Interprogrammsprung.
Und zuletzt haben wir uns gerade angeguckt, wie eben eine Methode
aufgerufen wird, wie diese Schachteln konstruiert werden, was die
Rücksprungadresse und der Umgebungszeiger für eine Rolle spielt und
was dann passiert, wenn wir die Methode verlassen.
So, und damit verlasse ich nämlich auch die Präsentation hier und gehe
zum nächsten Thema.
Und zwar ist das das Thema Best Practices.
Jetzt in dem zweiten Teil der Vorlesung für heute werde ich Sie dann
nicht mehr mit Maschinendetails belangen.
Jetzt geht es tatsächlich wiederum nur um Java.
Und auch hier möchte ich gleich am Anfang...
Hoppla.
Warum?
Ah, jetzt.
Okay, also was erwartet Sie eigentlich heute?
Sie sollen natürlich nicht nur Java irgendwie programmieren lernen
hier in dieser Veranstaltung, sondern Sie sollen eben auch Gutes
programmieren lernen.
Und was Gutes Programmieren in Java ist, das kann man oft nicht
irgendwie durch so eine Gesetzessammlung erklären.
Also es gibt nicht irgendwo eine Vorschrift, wie Java programmiert
werden muss.
Es gibt natürlich Vorschriften, wie die Syntax aussehen muss, damit
ein Programm tatsächlich ausführbar ist.
Aber was ein gutes Programm ist, das macht sich an vielen etwas
weicheren Kriterien fest.
Und deswegen gibt es auch oft hier keine Regeln.
Das heißt jetzt auch nicht Regeln und Verbote des Java Programmierens,
was wir heute machen, sondern sogenannte Best Practices.
Also es gibt immer beim Programmieren sehr viele verschiedene
Möglichkeiten, genau das gleiche Ergebnis zu erreichen.
Aber nicht alle davon haben die gleiche Qualität.
Und deswegen hat sich halt über die Jahre in der Entwicklung haben
sich dann solche Praktiken entwickelt, wo dann viele Entwickler gesagt
haben, ja, das ist in Ordnung.
So macht man es, wenn man es gut machen will.
So ist es Ingenieursmäßig.
Und das sind eben diese Best Practices, die ich Ihnen heute vorstellen
will.
Das meiste davon hat mit dem Instanzenbegriff und Vererbung zu tun.
Und dort vor allem, wie man Elemente vergleicht, die von
unterschiedlichen Klassen sind.
Und gerade deswegen schauen wir uns zum ersten...
Entschuldigung, ich muss noch den Laserpoint anschalten.
So, zuerst schauen wir uns hier an, wann man Polymorphie und wann
Instance of verwendet.
Und dann die nächsten Prinzipien, die wir uns anschauen, Prinzip zwei
bis sechs, handeln vor allen Dingen vom Vergleichen von Objekten.
Also Unterschiede zwischen dem Istgleichoperator und Equals ist etwas,
was Ihnen sicher auch schon begegnet ist.
Oder wo Sie sich vielleicht schon mal gewundert haben, warum sich ein
Programm komisch verhält.
Und vielleicht heute da eine Erklärung dafür finden.
Dann, wie man diese Equals-Implementierung überschreiben kann.
Was man dabei beachten muss, das ist nämlich, da gibt es ein paar
Stolperfallen.
Wie man da eben die Equals-Methode der Oberklasse verwenden kann, ist
fünftens.
Und dann nochmal die Stolperfallen, wenn man Instance of verwendet.
Jetzt können Sie sich ja fragen, naja, wir haben doch schon mal vor
ein paar Wochen diesen Foliensatz gehabt, wo auch schon so
Entwurfsprinzipien drin standen.
Warum denn jetzt nochmal solche Prinzipien hier?
Das, was wir uns damals angeguckt haben, ist eigentlich generell für
objektorientierte Programmiersprachen gültig, mehr oder weniger.
Und das, was wir uns heute angucken, ist also tatsächlich Java
-spezifisch.
Die Lernziele dementsprechend sind, dass Sie diese Best Practices
kennen und dass Sie auch wissen, wo sie angewendet werden.
Also hier, das sind oft auftretende Probleme, wollte ich es jetzt
nicht mal nennen, sondern das sind einfach Entscheidungen, die man
treffen muss.
Man weiß jetzt, man implementiert etwas, man hat viele verschiedene
Möglichkeiten, das zu machen.
Welches ist denn jetzt eigentlich eine gute und warum?
Deswegen sollen Sie die kennen und vor allen Dingen geht es hier im
Vergleich von Objekten, habe ich schon gesagt, natürlich.
Zweiter Punkt, hier sollen Sie auch das Ganze anwenden können.
Nicht nur die kennen und sagen, ja die gibt es und wie funktioniert
es, ja keine Ahnung, das ist natürlich nicht das Ziel heute.
Sie sollen die tatsächlich auch in Ihren eigenen Programmen anwenden
können.
Dann sollen Sie aber natürlich auch wissen, warum man das so macht und
was da der Zweck dafür ist.
Und ja so das Ziel auf der höchsten Ebene, was wir Ihnen eigentlich
ermöglichen wollen, ist, dass Sie gut lesbaren und gut wartbaren Code
schreiben können.
Denn das kennen Sie alle, Sie haben irgendwie Code von einem Kollegen
gezeigt bekommen, der sagt irgendwie, es funktioniert nicht, guck doch
mal drauf.
Hast du eine Ahnung warum und Sie brauchen erstmal eine Viertelstunde,
bis Sie verstanden haben, was so ein einfaches Programm tut.
Das kann man eben verhindern, indem man sich an solche Prinzipien
hält, die wir heute kennenlernen.
Auch zum Anfang gleich nochmal den Literaturhinweis.
Das Ganze ist aus einem Grundlagenbuch für Java entnommen.
Deswegen ist es auch schon ein bisschen älter, das ist schon aus dem
Jahr 2000.
Aber die Prinzipien gelten heute immer noch, weil sie halt wirklich
den Sprachkern von Java betreffen und deswegen auch in dem modernen
Java Programm, was 2017 geschrieben wird, im Prinzip immer noch gültig
sind.
Gut, dann fangen wir doch aber gleich schon mal mit dem ersten Prinzip
an.
Polymorphie ist zu bevorzugen gegenüber InstanceOf.
Ja, was heißt denn das jetzt erstmal?
InstanceOf kennen Sie den Operator, der bestimmt zur Laufzeit zu
welcher Klasse ein Objekt gehört.
Also Sie können dann tatsächlich, während Sie das Programm schreiben
und haben irgendein Objekt, was in irgendeiner Variable abgelegt ist.
Und Sie wissen eben zu dem Zeitpunkt, wo Sie das Programm schreiben,
noch nicht, was das für ein Typ hat, weil das eben je nach
Aufrufsszenario was Verschiedenes sein kann.
Können Sie eben mit diesem InstanceOf überprüfen, was ist denn der Typ
zur Ausführungszeit, zur Laufzeit.
Das aber ist natürlich sehr praktisch, wird aber deswegen häufig im
falschen Kontext eingesetzt.
Gerade weil es so einfach ist, denkt man, das sei die Lösung für
vieles, aber wie wir gleich sehen, ist es nicht immer.
Und in vielen Fällen kann man das durch Polymorphie ersetzen.
Polymorphie, wir erinnern uns, das ist eben, dass verschiedene
Operatoren, je nach Typ, sich eben unterschiedlich verhalten können.
Beziehungsweise, dass man eben auch Methoden in einer Unterklasse dann
auch überschreiben kann und mit einem anderen Verhalten versehen kann.
Ja, ich habe hier Ihnen jetzt schon mal ein Beispiel aufgeführt, steht
auch gleich in der Titelzeile drin, dass hier wahrscheinlich ein
schlechter Entwurf ist, aber jetzt schauen wir es uns doch mal an.
Also worum geht es?
Es werden irgendwie die Angestellten von irgendeinem Unternehmen
verwaltet.
Dafür gibt es so ein Interface, das nennt sich Employee, also
Angestellter und die haben irgendwie ein monatliches Gehalt, die
Salary.
Dann werden jetzt verschiedene Klassen definiert hier erstmal, die
diese Schnittstelle implementieren.
Der erste ist der Manager und wir haben jetzt hier mal der Einfachheit
halber fest hart reingecodet in dieses Beispiel, was so ein Manager
verdient.
40.000, hoffentlich pro Jahr, nehme ich mal an.
Pro Monat wäre ein bisschen viel, ist auch egal.
Und das hier ist also jetzt tatsächlich Static und Final, weil es
einfach nur ein festgelegter Wert sein soll für unser Beispiel hier,
würde man natürlich in Wahrheit anders machen.
Und es gibt dann eben so eine Art Getter-Funktion, die heißt nicht mal
Get, die heißt einfach nur Salary, die gibt eben diesen Wert zurück.
Ja, der Programmierer, der hat es schon besser, der verdient nämlich
mehr, also Sie sehen schon, das ist ein relativ idealisiertes
Beispiel.
Oft ist es natürlich so, dass der Programmierer leider weniger
verdient als der Manager.
Und nicht nur das, er kriegt auch noch möglicherweise, wenn er gut
gearbeitet hat, einen Bonus drauf.
Kennt man sonst aus den Nachrichten auch anders, dass eher die Manager
die Boni kriegen.
Aber hier in unserer kleinen Traumwelt ist es nun mal so, dass dann
die Programmierer auch die Boni kriegen und die Manager nicht.
Und dementsprechend gibt es hier auch einen Setter dafür.
Also was möchte man jetzt damit machen?
Man möchte irgendwie so eine Gehaltsabrechnung erstellen und möchte
berechnen, was hat denn so ein Angestellter verdient.
Also holt man sich erstmal eine Zahl per Salary von diesem
Angestellten, dann möchte man noch den Bonus ausrechnen.
Dann erinnert man sich aber, an unserer schönen Traumwelt hier,
kriegen nur die Programmierer, kriegen Boni, die Manager nicht.
Also überprüfe ich doch mal, ob dieser Employee tatsächlich auch eine
Instanz von Programmierer ist.
Und wenn das so ist, dann weiß ich, hier unten gibt es einen Bonus,
also muss ich den noch hier dazurechnen und rechne mir dementsprechend
aus, was das Ganze dann halt ist mit Summenbildung und gebe dann
zurück, wie viel er verdient hat.
Letztendlich möchte ich es dann irgendwie auf der Konsole ausgeben,
mache mir jetzt ein kleines Beispielprogramm in main, wo ich eine neue
Gehaltsabrechnung erstelle, einen neuen Programmierer, Manager und
gebe aus, was verdient der Programmierer, was verdient der Manager und
fertig.
Soweit, so gut, aber wir hatten ja in der Titelzeile stehen, das sei
schlechter Entwurf.
Deswegen ist jetzt die Frage, warum ist denn das schlecht?
Naja, also erstmal, hier kommt das Instance of Fort, das haben wir
gerade schon gesehen.
Das macht also das Java Laufzeitsystem, das überprüft dann zur
Laufzeit von dem Employee, ist es ein Programmierer oder halt nicht.
Denn hier oben haben sie ja nur stehen, Employee, das ist also quasi
der statische Typ, der zur Zeitpunkt des Programmierens festgelegt
wird.
Das ist nur die Schnittstelle, da können sie noch nicht wissen, ob
dann tatsächlich dieses EMP ein Programmierer ist oder ein Manager.
Aber woran merken wir jetzt, dass es schlechter Entwurf ist?
Was passiert denn, wenn wir noch weitere Klassen jetzt hinzufügen
wollen?
Also zum Beispiel haben wir nicht nur einen Manager, sondern wir haben
noch einen Executive Vorstandsmitglied oder wie auch immer man das
nennen will.
Ja und jetzt kommt der Punkt, wo Sie sich kurz was überlegen dürfen,
warum ist denn das jetzt schlecht bzw.
was passiert, was müssen Sie machen, wenn jetzt so ein Executive
hinzugefügt wird?
Machen wir das wie schon mal vor zwei, drei Wochen.
Überlegen Sie sich kurz selbst, wenn Sie meinen, Sie haben ungefähr
eine Lösung, diskutieren Sie es mit Ihrem Banknachbarn.
Wenn Sie keinen Banknachbarn haben, dürfen Sie auch ein bisschen
zusammenrutschen.
Ich gebe Ihnen drei Minuten dafür Zeit und dann tragen wir die
Ergebnisse hier vorne zusammen.
So, ich glaube es waren erst zwei Minuten, aber trotzdem, vielleicht
haben Sie ja schon eine Idee, was jetzt passiert.
Also was passiert, wenn wir noch den Executive hinzufügen?
Melden Sie sich, trauen Sie sich, hat letztes Mal auch gut
funktioniert.
Ja, hier vorne.
Genau, es gibt ein Problem, wenn der Executive auch einen Bonus
kriegen soll.
Und was ist dann das Problem?
Ja,
genau, da muss also noch ein Instance of hinzufügen.
Ich hoffe, Sie haben nicht gespickelt in die Folien, das steht
natürlich da schon drauf.
Also das Problem ist, wenn dieser Executive möglicherweise auch einen
Bonus haben soll, wird der einfach hier nicht berücksichtigt, weil
hier nur davon ausgegangen wird, nur Programmierer bekommen einen
Bonus und man müsste dann das erweitern durch Instance of.
Ja, und dann fragt man sich vielleicht noch, was ist denn hier jetzt
das Problem, denn es ist schlechter Entwurf.
Das Problem ist natürlich, dass Sie jetzt nicht nur eine Klasse
hinzufügen müssen, sondern Sie müssen halt auch hier, ich nehme wieder
den Laserpointer, Sie müssen tatsächlich die Implementierung Ihrer
Payroll anpassen.
Und das ist nicht das, was man sich eigentlich wünscht.
Wenn man das Programm entwickelt hat mit der Payroll und hinterher
wird an der Struktur des Unternehmens was geändert und man muss dann
das Programm nochmal tatsächlich aufmachen und die Implementierung
ändern, ist das eigentlich ein schlechter Entwurf, denn man hat sich
nicht vorher darüber Gedanken gemacht, was eigentlich passiert, wenn
sich sowas ändert und sowas kann sich ja mal ändern.
Deswegen habe ich hier ein Beispiel für einen besseren Entwurf.
Dort wird tatsächlich jetzt schon im Interface festgelegt, dass es
einen Bonus gibt.
Und es gibt dann eben, deswegen müssen dann auch alle von den Klassen,
die das eben implementieren, auch dann einen Bonus zurückgeben.
Dann würde hier eben beim Manager gesagt, es gibt keinen Bonus, er
muss aber hier eine Funktion haben, die den Bonus zurückgibt.
Dann beim Entwickler, Programmierer wäre der dann 10.000.
Und so könnten Sie jetzt ganz einfach eine neue Klasse hinzufügen,
denn dass es einen Bonus irgendwie gibt, das wussten Sie schon, als
Sie hier das implementiert haben.
Und deswegen können Sie das dann auch gleich in die Schnittstelle
hochziehen und Sie müssen hier nichts weitermachen, als einfach nur
das Aufrufen mit Salary und Bonus und zusammenzählen.
Und dann können auch neue Klassen hinzukommen, die von die Employee
implementieren.
Das stört Sie dann nicht, solange Sie bei der Bonusklasse irgendwas
zurückgeben und das müssen Sie.
Gut, also haben wir schon gesehen, warum Instance Off Probleme machen
kann, denn Sie müssen einfach wissen, welche Klassen da sind.
Und wenn noch welche hinzukommen, müssen Sie dann tatsächlich Ihre
Implementierung ändern.
Dann gehen wir weiter zum zweiten Prinzip, was Sie sich sicherlich
schon mal gefragt haben.
Es gibt ja in Java dieses istgleich, istgleich, um zu vergleichen und
außerdem gibt es auch equals.
Kann man nicht einfach alles mit diesem Doppelistgleich abfrühstücken?
Wozu braucht man denn überhaupt diese equals-Methode?
Ja, dazu habe ich wiederum ein Beispiel, das ist sogar noch einfacher
als jetzt.
Also dort gucken wir uns jetzt mal nur Zahlen an.
Was passiert?
Ich habe hier eine Testklasse, ich instanziere, genau, ich mache eine
Main -Methode.
Dann habe ich hier zwei Variablen a und b, die sind vom Typ int,
werden beide auf 10 gesetzt, wie Sie sehen, und ich gebe aus, was ist
a doppelistgleich b, was ist davon das Ergebnis?
Unten habe ich zwei Integer-Objekte, das gibt es ja in Java auch, es
gibt also nicht nur den primitiven Datentyp int, sondern auch Integer,
instanziere die auch mit 10 und gebe dann auch aus, was passiert hier
beim Doppelistgleich.
Ja, wie Sie sich schon denken können, kommt da was unterschiedliches
dabei raus, nämlich a istgleich istgleich b ist true und i a istgleich
istgleich b ist false.
Das ist jetzt natürlich seltsam und vor allen Dingen noch seltsamer
wird es, wenn Sie dann halt irgendwie so eine Testklasse da
mitschreiben.
Also ich mache jetzt irgendwie eine Main-Methode, die tatsächlich auch
was ausgibt.
Ich gebe aus, a ist so und so viel, b ist so und so viel und dann
wieder das gleiche wie links.
Dann steht da nämlich komischerweise da, a ist 10, b ist 10, aber a
istgleich istgleich b ist true und beim anderen ist es false.
Ja, wie kommt das denn jetzt, dass es einmal true ist und einmal
false?
Sie ahnen es schon, es hat irgendwas hiermit zu tun, dass es da ein
primitiver Datentyp ist und da eben diese Integer-Klasse.
Und ja, die Frage, warum verhält sich das Programm in dem Fall so?
Das ist deswegen, weil primitive Datentypen, die haben keine
korrespondierenden Objekte.
Also diese Variablen, die wir da als primitive Datentypen angegeben
haben, die haben dann keine Objekte, im Gegensatz dafür diese i b und
i a schon.
Die sind vom Typ Integer und deswegen zeigen die auf ein bestimmtes
Objekt.
Und wenn man dann istgleich istgleich aufruft, vergleicht man
tatsächlich, ob es die identischen Objekte sind.
Und das schlägt natürlich fehl, weil i a und i b sind zwei
unterschiedliche Objekte.
Ob die jetzt beide eine 10 drin haben, ist egal.
Die sind einfach unterschiedlich, stehen auch an unterschiedlichen
Stellen im Speicher und werden deshalb beim Vergleich dann als false
ausgewertet.
Ja, können Sie nicht.
Also Sie können es mit istgleich istgleich vergleichen, aber dann
kriegen Sie wahrscheinlich... Das ist eine gute Frage.
Also mit equals können Sie es nicht vergleichen, das kriegen wir
gleich, weil equals auf dem primitiven Datentypen nicht ausgeführt
werden kann.
Wenn Sie es mit istgleich istgleich vergleichen, weiß ich nicht, ob
das sogar schon im Syntaxcheck fehlschlägt.
Ich glaube nicht, dass der istgleich istgleich Operator primitive
Datentypen mit Objekten vergleichen kann.
Also ich weiß es nicht auswendig, aber ich würde annehmen, dass das
schon beim Syntaxcheck fehlschlägt.
Das ist eine gute Frage, kann ich Ihnen aus dem Bauch nicht
beantworten.
Nee, das müssten Sie mal ausprobieren.
Das weiß ich aus dem Bauch leider nicht.
Okay, gut.
Ja, ich habe es schon gehört, es gibt einen Fehler, hat gleich jemand
live gekodet und ausprobiert oder wusste es aus dem Kopf.
Aber ich hätte auch angenommen, dass es einen Fehler gibt.
Okay, aber dann ist die Frage, das ist ja vielleicht nicht das
Verhalten, was wir wollen.
Und wie vergleichen wir denn diese Objekte dann?
Und dafür haben wir den Equals-Operator.
Der liefert einen semantischen Vergleich.
Also der weiß tatsächlich, dass diese Integer-Objekte eben eigentlich
nur durch ihre Werte definiert sind, durch den Zahlenwert und kann
dann eben dementsprechend einen Vergleich machen.
Aber wie gesagt, Equals können Sie nicht auf den primitiven Datentypen
aufrufen.
Dann kann man natürlich damit wieder weitere komische Sachen machen.
Also wenn Sie jetzt das Beispiel ein bisschen anpassen, Sie nehmen
hier aber kein Int, sondern, also A ist immer noch Int, B ist aber
Float und vergleichen es dann mit Doppelistgleich.
Und unten haben Sie die korrespondierenden Objekte.
Dann passiert bei der Programmausgabe folgendes, da steht dann hier A
ist 10 und B ist 10.0, weil Floats eben standardmäßig so ausgegeben
werden, dass da eben der erste Nachkommawert angezeigt wird, auch wenn
es eine glatte Zahl ist.
Aber trotzdem macht der Doppelistgleich-Vergleich, wertet nach True
aus, wohingegen das bei den unteren Integer- und Float-Objekten nicht
der Fall ist.
Und das ist deshalb, weil es in Java sowas gibt, das nennt sich
Widening Primitive Conversion.
Sie können also Floats und Integers, solange Sie halt den Raum
erweitern, in dem die Zahlen quasi drin sind, können Sie einander
zuweisen, können es auch vergleichen.
Also Sie können aus dem Integer ein Float machen, denn mathematisch
wissen Sie, also die ganzen Zahlen, die sind eben eine Teilmenge der
rationalen Zahlen in dem Fall.
Und genau, andersrum geht es halt nicht, deswegen nur erweitern.
Ja, und warum kommt unten Falls raus?
Weil nämlich Objekte unterschiedlicher Klassen nie gleich sind.
Also natürlich können Sie in der Equals-Methode, wenn Sie sie selbst
definieren, alles Mögliche reinschreiben, aber die
Standardimplementierung der Standardklassen in den Bibliotheken von
Java sind eben so, dass da nie gleich rauskommt.
Genau.
Dann habe ich ja gerade schon gesagt, was Sie machen können, ist die
Standardmethode Equals überschreiben in Ihren eigenen Klassen.
Und das kommt auch jetzt als nächstes dran.
Da gehen wir jetzt mal ein bisschen weg von diesen einfachen
Beispielen mit den Zahlen, aber was Sie sich eben von dem Beispiel
gerade eben noch merken können, bei Doppelistgleich wird halt immer
wirklich die Objektreferenz verglichen und bei dem Equals sollte es
irgendwie ein bisschen ein semantischer Vergleich sein und der so ein
bisschen darüber Bescheid weiß, was auch der Inhalt und was die
tatsächliche Bedeutung dieser Klasse ist.
Das können Sie dann natürlich sehr gut machen, indem Sie sowas selbst
überschreiben.
Ich habe jetzt hier ein Beispiel auf den folgenden Folien, da geht es
nicht mehr um Zahlen, da geht es dann um Golfbälle.
Ich weiß nicht, ob irgendjemand von Ihnen Golf spielt.
Ich spiele kein Golf, aber ich musste nachschauen.
Solche Golfbälle, die werden anscheinend danach unterschieden, was sie
für eine Kompression haben, also wie stark sich so ein Golfball halt
verformt, wenn man mit dem Schläger draufhaut.
Natürlich hat der noch irgendwie eine Marke und von mir aus ein
Modelltyp, das ist also Brand und Make.
Also Sie haben irgendwelche Golfbälle, die sind halt von irgendeinem
bestimmten Hersteller, eine bestimmte Marke, dann ein bestimmtes
Modell und die haben halt diese Kompressionen als Zahl.
Dann ist auch nicht weiter spannend, was bei dem Konstruktor hier
passiert, da werden nämlich genau diese Dinger gesetzt, wobei
Hersteller und Modelltyp einfach ganz normale Strings sind und diese
Kompressionen kann man irgendwie mit einer Integerzahl angeben und
dann hier haben Sie quasi drei Getter, die Ihnen genau das wieder
zurückgeben.
Jetzt wollen wir aber sagen, so ein Golfball, das ist uns jetzt nicht
genau wichtig, ob tatsächlich das derselbe Golfball ist, wenn man es
mal auf Deutsch sagt, es reicht uns, wenn es irgendwie die gleichen
Golfbälle sind.
Also wenn ich jetzt zwei unterschiedliche Golfbälle habe, die aber die
gleichen Eigenschaften haben, also gleicher Hersteller, gleiches
Modell, gleiche Kompression, dann sollen die für uns genau gleich
sein.
Das ist also das Ziel, was wir jetzt mit unserer Equals-Methode
ausdrücken wollen.
Was passiert jetzt aber, wenn wir einfach diese
Standardimplementierung nehmen?
Also wir haben einfach das hier geschrieben in Java, haben die Equals
-Methode noch gar nicht selbst definiert.
Ja und jetzt machen wir mal ein kleines Testprogramm hier in unserem
Warehouse.
Wir haben also einen Golfball, der ist von der Marke X, Ausführung
Professional, Kompression 100 und 2 genau das gleiche.
Ja und wenn ich jetzt hier das Equals habe, ich rufe also diese
Standardimplementierung vom Equals aus und dann möchte ich eben
ausgeben auf der Kommandozeile oder auf der Standardausgabe Ball1
equals Ball2, ansonsten halt nicht.
Ja, was passiert jetzt aber, wenn ich das ausführe mit gb1 und gb2?
Da kommt natürlich aus, dass not equal, also muss wohl diese Equals
-Ausführung hier false zurückgeliefert haben.
Und warum?
Das ist ja irgendwie logisch, weil es sind ja zwei unterschiedliche
Objekte, die haben in diesem Fall die gleichen Eigenschaften und das
ist natürlich in der Standardimplementierung nicht so, dass einfach
die Eigenschaften verglichen werden.
Dieses Vergleichen von Eigenschaften, das vergleicht man ja manchmal
mit so einem Sprichwort, wo es darum geht, wann man einen Vogel als
eine Ente bezeichnet.
Es gibt später noch ein Konzept, was Sie in Softwaretechnik lernen,
das sogenannte Duck Typing.
Aber ich habe es mal nachgeschaut, das ist von einem Dichter namens
James Whitcomb-Ryle und der hat gesagt, When I see a bird that walks
like a duck, swims like a duck and quacks like a duck, I call that
bird a duck.
Und so wollen wir es halt hier jetzt auch machen, wenn wir einen
Golfball sehen, der die Marke hat, die Kompression und die Ausführung,
dann ist es uns egal, ob das das gleiche Objekt ist, sondern wir
wollen einfach nur die Eigenschaften vergleichen.
Macht aber die Standardimplementierung nicht.
Ja, das hat vorher im Prinzip, zwei hat es irgendwie funktioniert,
aber hier nicht.
Bei Integer war es schon klar, da haben sich halt die Leute, die diese
Integer -Klassen in Java implementiert haben, gedacht, so eine Integer
-Klasse, die hat als Eigenschaft eigentlich nur den Wert und deswegen
muss der Vergleich einfach die beiden Werte vergleichen und dann
wiederum true zurückliefern.
Der Golfball hingegen, der hat halt keine eigene Implementierung von
der Equals-Methode und was passiert dann, wenn man keine eigene
angibt, dann wird die allgemeinste genommen, nämlich die von java.lang
.object.
Also java.lang.object kennen Sie halt, die oberste Klasse in Java, die
allgemeinste.
Und ja, was passiert da jetzt?
Dann kann man mal reingucken in die Implementierung von dieser Equals
-Methode.
Ups, da hat er einmal zu viel geklickt, Entschuldigung.
Ja, die Implementierung der Equals-Methode in java.lang.object sieht
aber so aus.
Da wird einfach Doppel-Ist-Gleich verglichen.
Also im Prinzip, wenn Sie keinen Equals angeben für Ihre Klasse und
auch alle anderen Klassen, von denen Sie eben nicht irgendwas
Spezielles drinstehen haben, verhält sich Equals in Java standardmäßig
wie der Doppel-Ist-Gleich-Vergleich.
Und dann wissen wir auch jetzt, warum in dem Beispiel gerade eben, da
eben false zurückkam.
Das wollen wir aber natürlich nicht, deswegen überlegen wir uns, ja,
wir machen jetzt eine eigene Implementierung von der Equals-Methode.
Also wir haben hier wieder unsere Klasse Golfball.
Ich habe hier jetzt einfach ausgelassen, was wir schon hatten, die
ganzen Definitionen, Konstruktoren und Getter.
Und jetzt kommt hier eben die Equals-Methode, die wir selber definiert
haben.
Was macht die jetzt?
Erstmal gucken wir, schlägt der Ist-Gleich-Vergleich an.
Wenn das tatsächlich der Fall ist, wenn es wirklich dasselbe als
identischer Objekt ist, dann sind wir sowieso fertig.
Dann geben wir True zurück.
Falls das nicht der Fall ist, müssen wir uns was Komplizierteres
überlegen.
Also wir gucken erstmal, ist das Objekt nicht Null.
Das ist schon mal das Wichtigste.
Und dann benutzen wir hier die Get-Class-Methode von Java.
Also wir gucken, haben wir tatsächlich auch ein Objekt, was genau die
gleiche Klasse hat wie unser Objekt, mit dem wir es vergleichen
wollen.
Also es könnte ja sein, da wir hier nur Object stehen haben bei der
Equals -Methode, wir vergleichen einen Golfball mit einer Integerzahl.
Und das ist natürlich nicht sinnvoll.
Und bzw.
wir wollen dann einfach halt Falls zurückgeben.
Deswegen gucken wir hier, ob tatsächlich die Klasse identisch ist.
Ja genau, wenn wir dann wissen, dass die Klasse identisch ist, dann
können wir uns hier ein Objekt gb erstellen.
Und was jetzt passiert, hier oben hatten wir nur ein Object.
Aber hier können wir casten auf Golfball, weil wir ja gerade geprüft
haben, dass die Klasse tatsächlich identisch ist.
Ja und dann, wenn wir jetzt wissen, wir haben auch einen Golfball,
okay, dann können wir auch auf diesem gb-Objekt halt eben Brand und
Make und Compression ausführen.
Das sind diese drei Getter, die wir gerade definiert haben.
Und wenn die tatsächlich dann alle gleich sind, dann soll Var
zurückkommen.
Ja, was heißt jetzt aber gleich?
Gleich heißt hier Brand und Make, das sind ja Strings, die können wir
mit Equals vergleichen.
Da wissen wir, dass die Equals-Methode auf String auf der
Standardklasse so definiert ist, wie wir es gerne möchten.
Und bei Doppel ist gleich für das Integer ist es auch irgendwie
logisch.
Also kommt tatsächlich hier unten das raus, was wir wollen.
Nämlich, dass diese beiden Dinge, wenn sie für uns gleich aussehen und
die gleichen Eigenschaften haben, auch gleich sind.
Und das Ganze funktioniert also, wenn wir Attribute vom Typ String
haben, ganz genauso, wie wir es möchten.
Was ist jetzt natürlich hier das Problem?
Deswegen schreibe ich auch hier vom Typ String.
Wir könnten uns jetzt zum Beispiel überlegen, wir wollen für Brand und
Make keinen String verwenden, sondern String-Buffer, weil wir halt
irgendwie da gepufferte Strings verwenden wollen, aus verschiedenen
Gründen.
Und implementieren uns das Ganze einfach nochmal neu.
Alles genau wie vorher, nur mit dem Unterschied, hier steht eben jetzt
String -Buffer für Brand und Make.
Und auch die Rückgabe dann dementsprechend ist dann auch nicht vom Typ
String, sondern String-Buffer.
Ist ja erstmal keine große Änderung.
Man sollte meinen, String-Buffer verhält sich wie String, nur
gepuffert.
Aber dummerweise wird in der String-Buffer-Klasse die Equals-Methode
neu implementiert.
Beziehungsweise sie wird halt dort so implementiert, dass in diesem
Fall nicht das zurückkommt, wenn tatsächlich die Strings nur identisch
sind, sondern da ist bei Equals müssen es dann auch irgendwie müssen
andere Bedingungen halt gegeben sein.
Und es hängt eben ab von der Implementierung.
Also um zu wissen, warum das hier jetzt rauskommt, müssen wir uns die
Implementierung der String-Buffer-Klasse anschauen.
Und angenommen, wir haben das getan und merken, nee, das ist nicht
das, was wir wollen.
Ja, was machen wir denn dann?
Wir wollen aber trotzdem gern gepufferte Strings verwenden.
Mögliche Lösungsideen, die einfachste, wir gehen wieder zurück zu
String, ist aber doof, weil wir wollen diese Pufferung.
Und deswegen machen wir das natürlich nicht, sondern wir möchten
lieber was gepuffertes.
Und das kann nur dann gehen, wenn das tatsächlich die konkrete
Implementierung von String-Buffer so implementiert, wie wir es mögen.
Dann die zweite Möglichkeit ist, dass wir die Equals-Methode von
unserer eigenen Klasse Golfball so verändern, dass sie halt diese
String -Buffer-Objekte einfach zu Strings konvertiert.
Das kann man nämlich machen.
Man kann auf einem String-Buffer, wie Sie hier unten sehen, einfach
toString aufrufen.
Und dann wissen wir ja, bei Strings macht Equals das, was wir mögen.
Und dann können wir einfach hier das vergleichen.
Also wir machen hier einmal Brand to String und vergleichen dann GB
Brand to String.
Und dann haben wir genau das, was wir möchten eigentlich von der
Funktionalität, aber haben uns wieder ein neues Problem hier
eingehandelt.
Jedes Mal, wenn hier diese toString-Methode aufgerufen wird, wird ein
neues String-Objekt im Speicher erzeugt.
Und es sind schon bei diesem mini kleinen Beispiel hier, sind es schon
vier neue Strings, die hier erzeugt werden.
Und für die müssen alle erstmal Speicher freigeschaufelt werden.
Und jeder Java-Entwickler weiß intuitiv, dass alle Operationen, die
mit String zu tun haben, sehr, sehr teuer sind.
Was halt den Speicher betrifft und dementsprechend dann auch die
Laufzeit.
Denn Sie wissen, der Speicher muss dann durch die Garbage Collection
irgendwann wieder aufgeräumt werden.
Deswegen vielleicht doch auch nicht so eine gute Idee, da einfach
diese toString-Methode zu verwenden.
Also wir brauchen eine dritte Option.
Wir schreiben uns eine eigene String-Buffer-Klasse und überschreiben
die Equals-Methode, haben wir ja gerade gesehen.
Das kann man machen und benutzen da eine eigene Implementierung.
Das ist zwar ein bisschen aufwendiger, da müssen wir uns dann
überlegen, was wir da reinschreiben, aber vorteilhaft.
Das Dumme ist halt nur, dass String-Buffer eine Final-Class ist.
Von der können wir nicht weiter erben.
Also wir können diese Klasse nicht dadurch spezialisieren, dass wir
davon erben.
Aber wenn Sie sich mal zurückerinnern an den Foliensatz 15, wo wir ja
auch diese Entwurfsprinzipien hatten, haben Sie sowieso gesehen, dass
in manchen Fällen eben Composition over Inheritance zu bevorzugen ist.
Und das ist ganz genau das, was wir jetzt hier machen.
Wir machen uns also eine neue Klasse, die so eine Enthalten-Seins
-Beziehung hat.
Die erbt nicht von String-Buffer, aber die enthält dann String-Buffer.
Also wir schreiben uns hier so einen My-String-Buffer.
Der hat dann als private Variable eben den ganz normalen String
-Buffer.
Und beim Konstruktor passiert auch nichts Spannendes.
Da wird der dann einfach diese private Variable, diese interne, auch
befüllt.
Und für die Länge geben wir einfach das Interne zurück.
Und wenn wir jetzt ein bestimmtes Zeichen haben wollen, was an einer
Stelle steht, delegieren wir das weiter an dieses Objekt, mit dem wir
diese Komposition haben.
Und dann, gut, wir haben schon gesagt, es wird ein bisschen aufwendig.
Dann müssen wir jetzt diese Methoden quasi weitergeben.
Und dazu gehört auch die Equals-Methode.
Bei den Links war es noch recht einfach, aber jetzt müssen wir uns für
die Equals-Methode was ausdenken.
Also wir haben wieder, wenn es tatsächlich dieselben Objekte sind,
dieselbe Objektreferenz, dann kommt etrue zurück.
Und dann hier wieder diese Überprüfung.
Ist es nicht null, ist es von der gleichen Klasse?
Dann können wir das runtercasten auf myStringBuffer, weil wir gerade
den Class-Vergleich gemacht haben.
Und gucken dann erst an, ist die Länge gleich?
Wenn das schon mal nicht stimmt, sind sie sowieso nicht gleich.
Dann gibt falls zurück.
Wenn die Längen gleich sind, dann iterieren wir jetzt hier einfach
drüber und vergleichen zeichenweise.
Wirklich an jeder Stelle steht dort das gleiche Zeichen.
Und wenn da mal dann irgendwo ein unterschiedliches steht, dann
brechen wir auch ab und geben falls zurück.
Und wenn wir ganz am Ende durchgekommen sind und es waren alle Zeichen
gleich und wir sind am Ende des Puffers angelangt, dann geben wir
etrue zurück.
Wir sehen also, ein bisschen aufwendig in dem Fall hier so eine Equals
-Methode zu schreiben.
Aber lohnt sich dann doch, weil man jetzt nämlich eine saubere Lösung
hat und da das Verhalten hat, was man möchte.
Und dann letztendlich kann man dann diesen MyStringBuffer dann auch
verwenden.
Wenn man jetzt das wieder in unser Golf-Beispiel zurück spielt,
benutzen wir jetzt halt hier einfach MyStringBuffer statt dem
StringBuffer.
Hier wird es ersetzt, da passiert nichts Außergewöhnliches.
Und hier haben wir eben gerade diese Vergleiche, die wir vorher auch
schon hatten.
Wir können einfach Equals drauf aufrufen, weil wir jetzt ja wissen,
wir haben das bei uns überschrieben mit der Semantik, die wir gerne
hätten.
Und hier bei Compression reicht es immer noch, da können wir den
einfachen Vergleich machen.
Ich habe aber noch eine vierte Lösungsmöglichkeit mitgebracht.
Man könnte sich auch überlegen, diese Equals-Methode überschreiben,
das war jetzt doch ein bisschen schwierig.
Und vielleicht berücksichtigen wir die nicht weiter, sondern machen
einfach so eine Compare-Methode und verändern dann einfach die Equals
-Methode von Golfball.
Also, dass ihr halt nicht Equals aufruft, sondern Compare.
Ja, und die Vorteile ist, dass wir nicht irgendwie groß weitere
Klassen implementieren müssen.
Aber es hat natürlich auch Nachteile, denn wenn Sie sich vorstellen,
dass diese Equals-Methode der Golfball-Klasse eine Compare-Methode
aufruft, gehen Sie ja davon aus, dass diese Klasse Compare auch
irgendwie implementiert.
Das ist jetzt bei Ihrem speziell für dieses Beispiel hergestellten
StringBuffer der Fall.
Aber angenommen, irgendwann möchte man die Implementierung mal auf was
anderes ändern und dann steht da Compare und dann, äh, Mist, jetzt
muss ich das ja auch neu implementieren.
Und vor allen Dingen der Nachteil ist, wenn Sie zu String zurückkehren
irgendwann, müssen Sie auch den Code verändern, denn Compare gibt es
ja in String erstmal nicht.
Gut, dann gehen wir weiter zum Prinzip Nummer 4.
Ja, wir haben jetzt gesehen, Equals-Methoden kann man überschreiben,
man kann sich eigene implementieren.
Aber da sollte man Vorsicht walten lassen.
Deswegen, ja, sollte man sich vielleicht vorher fragen, möchte man das
überhaupt?
Und deswegen, das vierte Prinzip ist jetzt relativ kurz geraten.
Das besteht eigentlich nur in diesen Fragen, die man sich selbst
stellen sollte, wenn man sich überlegt, möchte ich jetzt so eine
Equals -Methode selbst implementieren?
Ist die Frage, die erste Frage hier, ja, benötigt meine Klasse
überhaupt eine Equals-Methode?
Also in dem Fall jetzt, den wir gerade gesehen haben, ja, anscheinend
brauchen wir das für diese Golfball-Klasse.
Aber wir brauchen es halt tatsächlich nur, wenn wir so eine Art Enten
-Vergleich machen wollen, wenn wir uns die Eigenschaften anschauen.
Wenn es tatsächlich reicht, wenn wir sagen, es sind tatsächlich
dieselben Objekte, dann reicht es auch, dass wir da die Standard
-Equals -Methode von Object verwenden, dann sind wir auch glücklich.
Und was wir jetzt gerade gezeigt haben, wie Equals implementiert wird,
das können wir ja nicht unbedingt immer in allen Fällen machen.
Also man kann sich komplizierte Fälle vorstellen, wo das nicht so
einfach geht.
Und dann sollte man sich überlegen, ja, braucht man die Equals-Methode
überhaupt?
Dann ist das Nächste und da kommen wir dann auch mit den nächsten
beiden Prinzipien noch hin.
Was soll denn da überhaupt alles verglichen werden?
Sind es nur Objekte von derselben Klasse oder sollen auch Objekte
verglichen werden von allen Unterklassen?
Sollen wiederum die Unterklassen nur miteinander verglichen werden
oder soll man auch Objekte der Unterklasse mit der Oberklasse
vergleichen können und so weiter?
Denn wenn Sie so eine Equals-Methode definieren, dann erben ja alle
weiteren Klassen, die von dieser Klasse dann erben, auch die
Implementierung dieser Equals-Methode.
Und die muss dann auch tatsächlich auf allen diesen Unterklassen
funktionieren.
Und wenn Sie solche Vergleiche halt quer drüber machen wollen, Element
der Ober- mit Element der Unterklasse, muss die Implementierung das
auch widerspiegeln.
Dann ist eine Möglichkeit, mit der man ja das umsetzen kann oder was
man auch machen kann, ist eben, dass man super.equals verwendet.
Also Sie wissen, mit super in Java rufen Sie explizit die
Implementierung der Oberklasse auf.
Und dann, wenn Sie halt so eine Equals-Methode haben bei Vererbung,
wissen Sie ja, dass Sie die von der Oberklasse mitbekommen und dann
halt eben selbst überschreiben können.
Ja, und jetzt haben wir uns ja schon gefragt, was passiert denn, wenn
wir jetzt so ein Element der Basisklasse mit einem Element der
Unterklasse vergleichen wollen.
Dazu Anpassung unseres Golfball-Beispiels, damit wir was Konkretes
haben, woran wir es festmachen können.
Also ich möchte jetzt von dieser Golfball-Klasse erben.
Hier in dem Fall tatsächlich mache ich jetzt eine Verfeinerung durch
Vererbung, weil mir nämlich eingefallen ist, ja, es gibt doch noch
irgendwie so konstruktive Unterschiede bei so Golfbällen.
Manche bestehen aus zwei Schichten, manche aus drei Schichten.
Das ist also das hier, Two-Piece und Three-Piece.
Das möchte ich irgendwie noch in so einem Byte-Feld hinzufügen und
möchte damit dann eben eine Klasse MyGolfball machen, die erbt von
Golfball, die hat die anderen drei Attribute, hier StringMake und
Compression, aber eben auch noch Construction.
Hier im Konstruktor wird dann tatsächlich eben auch der Konstruktor
der Überklasse aufgerufen und nur für dieses zwei- oder dreiteilige
Konstruktionsfeld dann diese private Variable gesetzt.
Dafür gibt es dann auch ein Getter hier und bei der Equals-Methode
ruft es dann einfach auch... genau, bei der Equals-Methode passiert
jetzt Folgendes.
Wir machen es wieder wie vorhin, gucken ob tatsächlich Objektreferenz
gleich, Null und selbe Klasse und dann vergleichen wir hier nur, ob
dieses Konstruktionsmerkmal das gleiche ist und ansonsten gibt es
True.
Da, wenn Sie jetzt hinkucken, denken Sie, das ist doch irgendwie
komisch, das stimmt doch irgendwie nicht ganz oder das ist doch ein
komisches Verhalten und ja, es ist ein komisches Verhalten, denn hier
würden jetzt ja zwei Golfbälle schon als gleich eingeteilt, wenn nur
diese Konstruktionseigenschaft gleich ist und alles andere
unterschiedlich ist.
Und das ist dann so, dass Sie das in Ihrem Warehouse auch einbauen
können.
Also wenn Sie jetzt tatsächlich hier zwei Golfbälle vergleichen, die
sind halt beide irgendwie Two-Piece und dann gibt es das eben zurück
mit dem Equals, dann würde es tatsächlich auch hier Equals
zurückgeben, wenn hier die anderen Werte unterschiedlich sind.
Also hier steht irgendwie 100 und da steht 90 und trotzdem kommt
Equals zurück, weil halt in diesem Code vom Equals vorhin nur das
Konstruktionsmerkmal verglichen wurde, weil es nämlich nur die Equals
-Methode von MyGolfball aufruft und die anderen drei Variablen werden
gar nicht überprüft.
Also vor allem wird eben die Equals-Methode von der Oberklasse von
Golfball nicht ausgeführt und das muss man dann eben sich per Hand
drum kümmern.
In dem Fall habe ich die Equals-Methode hier nur ein klein bisschen
angepasst, also das ist alles noch das Gleiche, nur hier hinten sehen
Sie als zusätzliche Bedingung wird zuerst noch diese Super-Equals
-Methode aufgerufen und nur dann, wenn also schon diese drei Variablen
gleich sind, dann wird nochmal dieses Konstruktionsfeld verglichen und
dann gibt es es zurück.
So funktionieren auch tatsächlich sehr viele Standardimplementierungen
in Java, dass eben wenn in einer spezielleren Klasse noch eine
speziellere Eigenschaft hinzukommt, eben hier durch Super schon die
bestehende Equals-Implementierung mit eingebunden wird und dann halt
erweitert wird.
Also wenn Sie sie einfach nur stumpf ersetzen, wie gerade eben, wird
das nicht standardmäßig ausgeführt.
Was man auch machen kann, also die Lösung, die Sie gerade gesehen
haben, die ist ja ein bisschen verbos und lang.
Sie können auch einfach sagen, ich rufe doch einfach direkt diese
Super -Equals-Methode auf und wenn das passt, dann kann ich doch hier
gleich dieses Casting machen und einfach diese beiden
Konstruktionsmerkmale vergleichen.
Das ist aber gefährlich und deswegen steht hier auch Vorsicht.
Sie müssen dann in diesem Fall wissen, wie die Equals-Methode Ihrer
Oberklasse funktioniert, weil, Sie sehen es vielleicht selbst schon
durchs Hingucken, was ist hier das Problem?
Wir casten hier GB auf MyGolfBall, obwohl wir, also Objekt wird
gecastet auf MyGolfBall und GB zugewiesen, obwohl wir gar nicht
wissen, ob dieses OBJ überhaupt vom Typ MyGolfBall ist.
Das haben wir jetzt nicht überprüft.
Das muss also irgendwo in der Equals-Methode von der Oberklasse muss
irgendwie so ein Klassenvergleich, wie wir ihn gerade hatten, mit
GetClass schon drinstehen.
Das wissen wir jetzt zufällig, weil wir halt irgendwie diese Golfball
-Klasse kennen und wissen, da steht dieses GetClass drin.
Aber dazu muss Ihnen bekannt sein, wie die Equals-Methode der
Basisklasse funktioniert.
Was passiert also dann, wenn Sie dann jetzt noch die
Vererbungshierarchie erweitern wollen?
Also Sie wollen zum Beispiel zwischen GolfBall und MyGolfBall noch
eine Klasse hinzufügen.
Ja, da MyGolfBall eben super Equals aufruft, funktioniert das alles
fehlerfrei.
Und deswegen, wie gesagt, es wird in vielen Klassen in Java auch so
gemacht, wie ich es Ihnen hier gezeigt habe, können Sie das als
Daumenregel mitnehmen.
Also wenn die Basisklasse schon eine Equals-Methode hat und viele
haben das und Sie machen in Ihrer abgeleiteten Klasse wiederum eine
Equals -Methode, schreiben Sie, dann muss dort einmal innen drin super
.equals stehen.
Ja, Ausnahme ist natürlich, wenn Sie keine Klasse haben außer Object
und die ist ja irgendwie immer implizit drin.
Also wenn Sie eine Java-Klasse erstellen und nicht hinschreiben,
Extents von irgendwas, ist Object die implizite Basisklasse.
Und das ist also die Ausnahme.
Da müssen Sie dann das Equals nicht überschreiben, weil das wird
sowieso bei allen Klassen darauf wieder zurückgeführt.
Dann haben wir noch das allerletzte Prinzip heute.
Und zwar, wenn Sie dann Equals selbst verwenden und eine eigene
Implementierung dafür schreiben, können Sie auch weiterhin InstanceOf
verwenden.
Das hatten wir ja gerade ganz am Anfang schon mal, das InstanceOf.
Aber auch da gibt es dann so ein paar Faltstricke, die ich Ihnen hier
mal beispielhaft zeigen will.
Und dann Ihnen zeigen, wie man das vermeiden kann.
Also was haben wir bisher gemacht?
Wir haben Objekte in derselben Vererbungsebene miteinander verglichen
und haben das mit GetClass gemacht.
Also wir haben wirklich immer nur Golfball mit Golfball verglichen,
MyGolfball mit MyGolfball.
Aber man möchte ja vielleicht auch mal irgendwie quer rüber
vergleichen.
Man hat irgendwie diesen MyGolfball und den anderen, möchte die
vergleichen.
Möchte irgendwie sagen, mir sind nur die drei Eigenschaften wichtig,
diese Compression ist mir dann egal.
Aber wenn die beiden gleich sind, dann soll trotzdem irgendwie eine
Art von Gleichheit dabei rauskommen.
Und das ist eben genau das, was ich vorhin schon gesagt habe.
Manchmal möchte man eben auch abgeleitete Klassen und deren Objekte
mit Objekten einer Basisklasse vergleichen können.
Und wann ist sowas vor allen Dingen interessant, wenn die abgeleitete
Klasse zum Beispiel nur das Verhalten von der Klasse ändert?
Das kann ja oft sein, dass Sie irgendwie eine Implementierung ändern,
aber die strukturellen Dinge bleiben alle gleich.
Also die haben alle die gleichen Variablen noch, die gleichen
Eigenschaften.
Und deswegen sind sie auch vergleichbar, sie verhalten sich halt nur
anders.
Und jetzt ist eben die Frage, wie können wir das hinkriegen, dass wir
diese beiden vergleichen?
Mit GetClass haben wir vorhin schon gesehen, werden tatsächlich scharf
die Klassen verglichen.
Also MyGolfball und MyGolfball sind halt dann nur dann gleich, wenn es
tatsächlich die gleichen Klassen sind.
Weil GetClass halt wirklich eine Klasse liefert, die dann mit IsGleich
verglichen wird, ob es tatsächlich zur gleichen wird.
Bei InstanceOf kommt ja auch War zurück, wenn es eine Instanz von
einer Unterklasse ist.
Und deswegen könnten wir das ja dann verwenden, um dort auch für
Instanzen der Unterklassen irgendwelche Aussagen zu machen.
Dann ist aber das Problem, wenn Sie GetClass einsetzen, sind diese
Objekte halt niemals gleich mit den Objekten der Basisklasse, weil es
unterschiedliche Klassen sind.
Und dann kann die abgeleitete Klasse damit die Equals-Methode nicht
implementieren und auch keine weiteren Attribute berücksichtigen.
Das ist ja irgendwie doof.
Man möchte ja gerne diesen Quervergleich haben.
Ich habe Ihnen jetzt mal noch ein weiteres Beispiel als Motivation
gebracht, warum so eine Verhaltensänderung vielleicht notwendig sein
kann.
Jetzt hier sind es keine Golfbälle, weil die verhalten sich relativ
wenig, die sind eher passiv.
Also haben wir hier eine Klasse Auto, das hat irgendwie ein gewisses
Verhalten.
Und das hat auch wieder einen Hersteller und ein Jahr vielleicht, von
dem das Modell ist.
Und dann können Sie da natürlich Drive drauf aufrufen, um halt
irgendwie irgendwas anzusteuern.
Denken Sie an ein selbstfahrendes Auto oder sowas.
Und haben auch eine Equals-Methode, die aber nur sagen soll, ist es
tatsächlich der gleiche Hersteller und das gleiche Jahr.
Wenn Sie dann MyCar haben, möchten Sie vielleicht nur diese Drive
-Methode ändern.
Da passiert dann was anderes.
Dieses Fahren funktioniert anders, aber sonst strukturell oder von den
Eigenschaften, vom Zustand her, sind diese Objekte immer noch die
gleichen.
Also das ist ein Beispiel dafür.
Jetzt gehen wir wieder ein bisschen abstrakter zurück.
Wir hatten jetzt eine Klasse Car und MyCar oder Golf und Ball und
MyGolfBall.
Jetzt nennen wir die Klasse nur noch Base und Derived.
Also Base ist die Basisklasse und Derived ist die Klasse, die davon
ableitet.
Und wir haben jetzt hier ein Beispiel, wo so eine Equals-Operation, so
eine Equals-Methode schon in der Base-Klasse drin ist.
In der abgeleiteten Klasse ist aber noch keine drin.
Und jetzt ist halt die Frage, ich erstelle mir so ein Element B, was
mit B getypt ist und tatsächlich auch vom Konstruktor von Base
aufgerufen wird.
Dann so ein Derived-Element, das gleiche.
Und jetzt möchte ich wissen, sind die gleich?
Die haben irgendwelche Attribute und ich möchte halt sagen, naja, die
sind gleich, wenn die Attribute stimmen, ob jetzt die Klasse
tatsächlich dieselbe ist.
Das ist mir egal, solange es in der Vererbungs-Hierarchie drin ist.
Und deswegen liefert dieses Equals dann je nachdem, wie es
implementiert ist, dann auch True, auch wenn die beiden Klassen, wenn
die beiden nicht vom exakt gleichen Typ sind, aber halt die Equals
-Methode das irgendwie anderweitig berücksichtigt und sich nur
irgendwelche Attribute anschaut.
Zum Beispiel wie gerade eben nur die Jahreszahl und den Hersteller.
Und das Ganze geht dann natürlich auch in die andere Richtung.
Sie können dann auch ein Element der Oberklasse mit einem Element der
Unterklasse vergleichen.
Und damit haben Sie eine Symmetrie-Eigenschaft.
Also wenn D equals B ist, dann ist B equals D.
Und das wollen Sie ja auch, weil Equals, das ist irgendwie intuitiv
klar so Gleichheit.
Das ist symmetrisch, das wissen Sie aus Ihrer Mathe-Vorlesung.
Also wenn A gleich B, dann B gleich A.
Das ist irgendwie sinnvoll.
Und das gehört eben auch dazu, wenn man guten Code schreiben will,
dass man solche Dinge berücksichtigt.
Denn wenn Sie in Ihrer Implementierung das irgendwie so machen, dass
Equals in die eine Richtung geht und die andere nicht, und
irgendjemand benutzt Ihre Klasse, der das nicht weiß und der einfach
denkt, Equals ist für mich halt Gleichheit, ob ich jetzt das eine mit
dem anderen oder das andere mit dem einen vergleiche, muss doch
dasselbe rauskommen.
Und deswegen ist diese Symmetrie-Eigenschaft wichtig und man sollte
berücksichtigen, dass die dann auch immer gilt.
Wir haben jetzt ein Beispiel, wo es dann auf einmal plötzlich nicht
mehr gilt, wenn Sie jetzt nämlich hier auch so eine Equals-Methode
einführen unten.
Also Sie haben hier zusätzlich noch eine Equals-Methode in Ihrer
Derived -Klasse.
Die ruft auch die Equals-Methode von der Oberklasse auf.
Und ja, jetzt ist die Frage, wenn Sie Derived auf dieser...
Genau, wenn Sie das Derived-Element mit Base vergleichen, dann kommt
dann in dem Fall immer True zurück, weil hier oben wird ja nur
geguckt, ob das Instance of Base ist.
Und eigentlich wollen Sie hier unten vielleicht etwas Spezielleres
machen, aber das ist immer Instance of Base, weil auch hier Derived
ist ja erweitert Base.
Deswegen kommt hier immer True zurück.
Und da geht dann halt eben die Symmetrie verloren, weil es andersrum
nicht so ist.
Nämlich wenn Sie halt dann auf dem Objekt B das aufrufen und dann halt
Equals -D machen, dann geht da diese Symmetrie eben verloren, weil
diese Equals-Methode mit einem Objekt vom Typ Base aufgerufen wird und
da der Typ der Basisklasse ausgeführt wird.
Das Problem ist dann, was Sie hier oben haben, ist, dass das halt dann
in dem Fall auch True zurückliefert und es auch möglich ist, dass der
Vergleich irgendwie ungültig ist, aber trotzdem True zurückliefert.
Weil es kann ja sein, dass irgendwie auf den Unterklassen bei dem
Vergleich, der dann ausgeführt wird, bei dem Equals nicht immer True
zurückgibt.
Und deswegen haben Sie dann möglicherweise ein Verhalten, wo diese
Symmetriebedingung verletzt ist.
Und das ist nicht schön.
Sie können dann halt versuchen, hier unten eben nicht dieses Equals
von Super aufzurufen, sondern erstmal hier Instance of von Derived
reinschreiben.
Dann haben Sie aber das Problem, wenn Sie jetzt D Equals B aufrufen,
dann liefert es immer False, weil hier Instance of Derived stimmt ja
dann nicht, wenn Sie ein Objekt der Basisklasse vergleichen.
Und wenn Sie B Equals D aufrufen, wird natürlich dann Equals von der
Basisklasse verwendet, weil Sie hier auf B das Equals aufrufen.
Und dann liefert das immer True und dann haben Sie auch keine
Symmetrieeigenschaft.
Dann haben Sie auch noch zusätzlich ein Problem, nämlich das
Verhalten, dass halt diese Symmetrieeigenschaft nicht eingehalten
wird.
Das ist nicht intuitiv und auch fehleranfällig.
Wenn Sie dann den Programm testen wollen, müssen Sie nicht nur die
Schnittstellen wissen, sondern Sie müssen genau die Implementierung
kennen.
Also wenn Sie sich nur die Schnittstelle angucken und sagen, da steht
Equals, Equals heißt ja gleich.
Also muss es ja auch irgendwie gleichheit sein.
Das sollte das normalerweise reichen, so als Art Vertrag kann man sich
das dann vorstellen.
Aber nein, wenn Sie hier testen, müssen Sie tatsächlich in die
Implementierungsklassen reinschauen und gucken, wie dieser Equals
Operator ausgeführt ist, um das zu verstehen.
Und das gilt für alle, die diesen Code benutzen.
Es kann ja auch mal sein, dass Sie gar nicht den Quellcode haben von
irgendeiner Java-Bibliothek, die Sie benutzen oder irgendein Java
-Paket, laden sich runter.
Sie haben nur kompilierte Class-Dateien.
Sie haben nur die Schnittstellenbeschreibung und versuchen das dann
halt zu verwenden, entsprechend der Beschreibung, wie Sie vermuten,
dass das Objekt sich dann verhält.
Aber in dem Fall können Sie es daraus gar nicht vermuten oder es
verhält sich halt unerwartet.
Und das ist deswegen fehleranfällig.
Sie müssen dann irgendwie an den Quelltext rankommen und sich die
Implementierung anschauen und dann sehen, ah ja, hier dieser
Spezialfall, ok, hier ist es asymmetrisch.
Und deswegen sollte man das also nicht tun.
Genau, deswegen der dritte Punkt wichtig.
Das gilt halt für alle, die den Code benutzen.
Nicht nur die, die halt tatsächlich in der Entwicklung mit drin sind
und halt Einblick in die Quellen haben, sondern auch wenn Sie das Ding
irgendwann mal ausliefern und jemand benutzt es als kompiliertes Class
-File, dann hat er halt nicht die Möglichkeit, das genauso
nachzuprüfen.
Genau, und dann viertes, wenn man halt ein Objekt durch seine
Basisklasse referenziert, dann kann es halt auch möglich sein, dass
ein Objekt vom Typ Java lang Object zurückgegeben wird.
Und dann kann man nicht weiter nachprüfen, was es jetzt tatsächlich
für einen genauen Typ hat und kann die abgeleitete Klasse nicht genau
identifizieren.
Gut, dann möchte ich jetzt hier auf der Folie nochmal zusammenfassen,
was wir jetzt hier im zweiten Vorlesungsteil heute gesehen haben.
Also es geht darum, wie vergleiche ich Objekte miteinander.
Wir haben jetzt hier verschiedene Möglichkeiten der Lösung gesehen,
die alle so ihre Vor- und Nachteile haben.
Mein Tipp an Sie, probieren Sie es einfach mal selbst aus, vielleicht
nicht mit Golfbällen und mit Autos, sondern mit irgendeiner Klasse,
die Sie selbst implementiert haben.
Versuchen Sie mal, da halt so eine Klassenhierarchie aufzubauen und
dann eben diese Verfahren, die wir heute kennengelernt haben,
auszuführen.
Also das erste Verfahren wäre, dass wir halt der Basisklasse haben,
die so eine Equals-Methode implementiert und die abgeleitete Klasse
nicht.
Und da ist dann eben das Problem, bzw.
drittes Problem nicht auf.
Wenn die Basisklasse dann nämlich InstanceOf benutzt, dann kann man da
auch Objekte der Unterklasse mit vergleichen und dann ist es in
Ordnung, denn der Vergleich ist dann symmetrisch.
Und diese Symmetrie-Eigenschaft nochmal, das ist bei Equals etwas
Wichtiges und da sollten Sie immer schauen, dass die eingehalten wird.
Dann haben wir uns als zweite Methode angeschaut, dass sowohl die
Basisklasse als auch die abgeleitete Klasse diese Equals-Methode
implementieren und dann haben wir eben die Situation, wenn diese
Equals -Methoden beides mal InstanceOf benutzen, dann funktioniert es
in die eine Richtung, aber nicht in die andere.
Also es wird False zurückgegeben, wenn man die Equals-Methode der
abgeleiteten Klasse verwendet und es wird True zurückgegeben, wenn man
die Equals-Methode der Basisklasse verwendet.
Und das haben wir gerade gesehen, ist halt ein Problem, wenn man ein
Element der Basisklasse mit einem Element der abgeleiteten Klasse
vergleichen will, weil es dann so rum geht und so rum halt nicht.
Und deswegen ist die Symmetrie-Eigenschaft verletzt und das ist etwas,
was man nicht möchte.
Genau, dann kann man sich natürlich noch überlegen, die abgeleitete
Klasse implementiert die Equals-Methode, aber auf der Basisklasse
haben wir die gar nicht implementiert.
Dann liefern die zwar auch False zurück, aber dann eben aus
unterschiedlichen Gründen, denn wenn sie auf der Basisklasse dann
Equals aufrufen, haben wir gerade gesehen, dann wird eben die Equals
-Methode von Java LangObject schlimmstenfalls ausgeführt oder von
einer weiteren Oberklasse dieser Methode, wo das halt das erste Mal
definiert ist.
Vielen Dank für's Zuschauen.