Ich werde hier in dieser letzten Vorlesung zwei Dinge erklären.
Einmal so ein bisschen unter der Haube gucken an einigen Stellen, was
passiert eigentlich, wenn so ein Programm abläuft.
Wir haben also bisher ja immer nur programmiert, also die Eingaben
gemacht, aber wie das technisch umgesetzt ist beim Ablaufen, das ist
ein eigenes Gebiet.
Da werde ich nur einen kleinen Aspekt mal rausholen, aber einen, den
Sie auch im weiteren Verlauf des Studiums häufiger brauchen.
Zum anderen möchte ich am Ende noch mal ein paar Folien auch
verwenden, um so klassische Fehler, die man beim Implementieren in
Java gerne mal macht, von vornherein mal zu zeigen und eben auch zu
sagen, wie es richtig geht.
Und am Ende noch ein paar organisatorische Hinweise.
Ja, das Material aus dieser Vorlesung ist aus dem Buch entnommen, was
Sie in der Bibliothek finden sollten von Gerhard Goos, der
Gründervater hier der Karlsruher Informatikfakultät.
Wie gesagt, deswegen findet das auch die Bibliothek und da gibt es
einen zweiten Band von der Vorlesung über Informatikobjekte und jedes
Programmieren und da ist es das Kapitel 11.
Ich habe es ein bisschen angepasst, vereinfacht hier für den Java
-Kontext, aber da ist das Material her.
So und zwar, was ich jetzt in erster Linie mit Ihnen besprechen
möchte, ist, was passiert eigentlich bei einem Methodenaufruf.
Und auch noch so ein bisschen, was passiert eigentlich, wie wird
eigentlich das Programm ausgeführt, was Sie geschrieben haben.
Noch mal so das Überblicksbild vom Werkzeugen her, kennen Sie das
schon alles?
Sie haben Ihr Java-Programm geschrieben, das in einer .java-Datei
abgelegt, dann rufen Sie Java C auf, dann bekommen Sie eine .class
-Datei, also den Vornamen der Datei und dann .class dran.
Das ist ein Bytecode und der wird dann auf einer Java Virtual Machine
ausgeführt.
Der Grund, warum man das so gemacht hat, ist, dass man sagt, egal was
hier unten für eine Prozessorarchitektur drunter hängt, das ist immer
dieselbe.class-Datei, die ich nehmen kann.
Und das Umsetzen für die spezifische Prozessorplattform, das macht
eben die Java Virtual Machine.
Sie haben für die verschiedenen Betriebssysteme, für die verschiedenen
Prozessor -Hardware-Plattformen eben dann immer spezifische Virtual
Machines.
Aber bis hierhin ist immer alles gleich.
Das war so ein bisschen die Überlegung.
Es gibt zwei, drei Stellen in Java, wo das nicht geklappt hat, wo es
die Plattform doch durchschlägt.
Manchmal bei Dateinamen, ob das ein Slash oder Backslash ist, da merkt
man auch so Unix- oder Windows-Unterschiede.
Aber im Großen und Ganzen, das eigentlich funktioniert hier, die
Geschichte.
Jetzt ist es so, wenn... also der Compiler erzeugt eben diesen
Bytecode -Datei.
Wir gucken jetzt nicht unter die Haube, was hier passiert.
Hier passieren viele spannende Sachen.
Optimierungen zur Laufzeit, die Java Virtual Machine merkt zum
Beispiel, wenn Sachen wiederholt ausgeführt werden, versucht sie dann
zu optimieren und so.
Das machen wir alles nicht.
Sondern wir gucken uns eigentlich im Wesentlichen so diesen Weg hier
runter an.
Und insbesondere auch das Thema Speicheraufbau und was passiert eben
beim Methodenaufruf, weil das nämlich mit dem Speicher zu tun hat.
So und dazu... Begriffe, also es ist so, wir haben uns den Speicher
immer bisher in den Beispielen über die Arrays vorgestellt als quasi
lineare Folge von lauter Speicherzellen, die alle mit dem Index
adressiert werden.
Das ist im Prinzip auch richtig und ich will in die technischen
Details auch gar nicht weiter eintauchen.
In echt sind es natürlich Bytes.
Ich kann verschiedene größere Speicherblöcke haben.
Aber was ich jetzt hier nochmal quasi neu mache, was wir bisher immer
nur so bestenfalls auf der Tonspur hatten in der Vorlesung, ist,
dieser Speicher ist eingeteilt im Programm.
Speicher ist nicht gleich Speicher, sondern es gibt verschiedene
Speicherbereiche.
Und einen Speicherbereich gibt es, der heißt im englischen Heap oder
deutschen Halde.
Das ist der Speicherbereich, wo zur Laufzeit Objekte angelegt werden.
Also immer wenn wir sie New aufrufen, hatte ich Ihnen erzählt, beim
New -Aufruf wird, bevor der eigentliche Konstruktor aufgerufen wird,
erstmal Speicher für dieses Objekt besorgt und zwar von der Halde.
Die Speicheradresse wird dann zurückgegeben und in die Referenz auf
das Objekt eingeschrieben.
Und das ist dann ein Speicherbereich, der eben auf dieser Halde zeigt.
Und wie man das jetzt hier in dieser Abbildung sieht, es gibt die
maximale Adresse, die das Programm adressieren kann.
Und dann gibt es ab MinHeap einen Speicherbereich, das ist die Halde.
Oben drüber gibt es auch gültige Adressen, aber die gehören dann nicht
zur Halde.
Und wie man jetzt hier so sieht, hier sind immer mal so Sachen
eingefärbt.
Das soll sagen, das ist belegt, aber wie Sie sehen, das ist
unregelmäßig belegt.
Also der Algorithmus, der abläuft im Betriebssystem, wenn Sie New
aufrufen, wenn Sie neuen Speicher besorgen, der dann rausfindet, an
welcher Stelle habe ich jetzt eigentlich genug Speicher, um diese
Speicheranfrage zu befriedigen.
Dieser Algorithmus ist relativ kompliziert, taucht bei Ihnen nachher
mal in der Betriebssysteme-Vorlesung auf.
Im Wesentlichen, was der macht, ist, der hat eine Freispeicherliste,
der weiß also, wie große Blöcke freien Speicher sind und versucht eben
da möglichst schnell, aber einen möglichst gut passenden Block auch zu
finden.
Da aber jetzt, können Sie sich vorstellen, mehrere Objekte erzeugt
werden, dann wieder einige davon auch wieder freigegeben werden
können, weil sie nicht mehr benötigt werden, dann kann es eben
passieren, dass der Speicher so fragmentiert und so Lücken entstehen.
Und das ist aber ein ganz klassisches Charakteristikum in diesem
Speicherbereich Halde, dass der eben fragmentiert ist.
Wenn das Ganze zu sehr fragmentiert ist, was immer jetzt zu sehr
heißt, dann läuft ein sogenannter Garbage-Collector oder auf Deutsch
Speicherbereiniger drüber, der das mal wieder zusammenschiebt, hier
die Bereiche.
Weil es ist natürlich klar, wenn der sehr stark fragmentiert ist,
hätten Sie im Prinzip genug Platz noch für ein großes neues Objekt,
was Sie anfordern wollen, aber da Sie den Speicherplatz eben nicht am
Stück haben, können Sie das Speicherobjekt nicht anlegen im Speicher.
Deswegen muss der nur wieder mal defragmentiert werden.
Da brauchen Sie sich gar nicht drum kümmern als Programmierer, das
macht das Java-System für Sie im Hintergrund, dieser Garbage
-Collector.
Da der aber irgendwann mal loslaufen kann, haben Sie manchmal so ein
interessantes Erlebnis, dass auf einmal dieses Java-Programm extrem
langsam wird, dann läuft gerade der Garbage-Collector und danach aber
geht wieder alles weiter und Sie fragen sich, was war jetzt los?
Und genau, also performance-mäßig merkt man diesen Garbage-Collector,
wenn der mal losläuft.
Nichtsdestotrotz, es gibt auch Spezialvarianten von Java, zum Beispiel
für eingebettete Systeme, wo die Speicherverwaltung so gemacht wird,
dass man keinen Garbage-Collector braucht, weil das natürlich so alles
an planbarem Echtzeitverhalten kaputt macht, so was.
Aber das jetzt erst mal, wie gesagt, zum Begriff Halde oder englisch
Heap.
Da gibt es einen anderen Speicherbereich, das ist der Stapelspeicher
oder auch Kellerspeicher.
Im Englischen wird das Stack oder Stapel genannt.
Das ist eine etwas speziellere Datenstruktur und zwar kann auf diesen
Speicher in einer bestimmten Reihenfolge nur zugegriffen werden.
Der Heap, den wir eben hatten, die Halde, ist frei adressierbar, die
ist indiziert.
Beim Stapelspeicher ist es daher, da haben wir auch Stapel, so dass
ich eine sogenannte Last-In-First-Out, also der Letzte, der reinkommt,
ist auch der Erste, der wieder genommen wird.
Und ich kann jetzt nicht beliebig zugreifen, sondern ich kann halt
quasi wie beim Kartenstapel immer nur auf die oberste Karte zugreifen,
aber ich kann mir nicht alle Mitten so rauszuppeln.
Dazu gibt es zwei Operationen, eine Push und eine Pop-Operation.
Push heißt, ich lege ein neues Objekt auf diesen Stapel drauf und Pop
heißt halt, dass ich das wieder herunternehme.
Und dieser Stapelspeicher, das ist der, den wir auch brauchen beim
Methodenaufrufen.
Jetzt sehen wir uns erstmal hier ein Beispiel.
Ich habe jetzt so eine Befehlsreihenfolge Push und nochmal Push B und
dann Push C.
Das baut sich also, hier sehen Sie so quasi als Stapel auf und Push D
obendrauf und dann hole ich mit Pop das Element D wieder runter.
Das ist auch vollkommen klar, Pop wirkt immer automatisch auf das
oberste Element.
Also ist klar, dass das dann das D ist.
C und mit B hole ich es wieder weg und der Stapel baut sich auch
wieder so ab.
Wie ich gesagt habe, der Stapelspeicher ist für uns deswegen wichtig,
weil er genutzt wird, um beim Methodenaufruf wichtige Werte zu
speichern.
Und zwar sind das eine ganze Reihe von Werten, die gespeichert werden
müssen auf dem Stapel.
Und zwar, wenn Sie eine Methode aufrufen, wissen Sie ja, Sie können
dieselbe Methode von verschiedenen Stellen im Programm aufrufen.
Das werden Sie vielleicht gar nicht immer machen.
Manchmal haben Sie vielleicht eine Methode nur eingefügt, um das
Programm etwas besser zu strukturieren.
Vielleicht eine Private Methode, die nur an einer Stelle aufgerufen
wird.
Aber in der Regel ist es eben möglich, dass eine Methode von vielen
Stellen aufgerufen werden kann.
Also muss man sich auch merken, wenn die Methode fertig ist, wo muss
ich wieder hin zurück.
Und das ist genau diese Rücksprungadresse.
Wenn Sie das Wort Unterprogramm hier lesen, das ist eine
Verallgemeinerung von Methode.
Dann ist es so, Sie erinnern sich, wir hatten einen Rückgabetyp.
Wenn der nicht void war, dann wird tatsächlich was zurückgegeben.
Und auch das muss irgendwo ja dann dem Aufrufer vom Programm übergeben
werden.
Das ist die Rückgabe Werte eines Unterprogrammes.
Und dann muss ich natürlich noch die Parameter, die übergeben werden.
Sie erinnern sich, diese Transition von formalen Parametern zu
aktuellen Parametern.
Formale Parameter sind die, die Sie hinschreiben bei der Deklaration
und Definition Ihrer Methode.
Aktuelle Parameter sind dann die Variablen, die tatsächlich übergeben
werden als Parameter.
Und irgendwie müssen die ja jetzt übergeben werden an die Methode.
Die müssen von dem Code der Methode aus nutzbar sein.
Und auch die kommen hier auf den Stapel drauf.
Und schlussendlich natürlich auch noch lokale Variablen.
Wir haben uns erinnert, es gibt Variablen, die ich eben nicht als
Attribute einer Klasse angebe, sondern nur für die Methode zugreifbar
sind.
Das sind die lokalen Variablen und die werden auch auf dem Stapel
gespeichert.
Das heißt, wenn die Methode fertig ist in ihrer Abarbeitung, wird der
Stapel auch wieder abgebaut und dann fallen auch automatisch diese
lokalen Variablen weg.
Jetzt ist es so, Sie können natürlich Methoden geschachtelt aufrufen.
Es gibt eigentlich nicht von vornherein eine Beschränkung, wie tief
Sie schachteln können, außer natürlich Ihr Speicherplatz, der endlich
ist.
Und das heißt, Sie brauchen immer wieder neuen Platz auf dem
Kellerspeicher, wenn Sie nämlich eine neue Methode aufrufen.
Und wenn diese Methode bevor sie fertig ist eine weitere Methode
aufruft, brauchen Sie noch etwas mehr Platz auf dem Kellerspeicher.
Dann aber ist zum Beispiel irgendwann mal die Methodenbearbeitung
vorbei und dann bauen Sie diesen Platz auf dem Kellerspeicher auch
wieder ab.
Das heißt, der Kellerspeicher, der oszilliert so und man nennt das
dann auf deutsch Pegel.
Das ist quasi der Pegel im Kellerspeicher, genau wie ein
Wasserstandspegel steigen und sinken kann.
Genauso ist es auch hier.
Je nachdem, in welcher Verschachtelungstiefe Sie sind beim Aufruf der
Methoden, entsprechend haben Sie dann einen höheren oder tieferen
Pegel.
Da das ganze Kellerspeicher heißt, wächst der halt nach unten.
Das hat man auch bewusst mal im Deutschen den Begriff so gewählt, weil
es tatsächlich im Programm layout so ist.
Der Kellerspeicher, der fängt hier bei MINVAR an und wächst dann hier
nach unten und wieder nach oben.
Und hier unten drunter liegt dann die Halde.
Im Prinzip steigt der Speicherbereich auf.
Kellerspeicher und Halde.
Halde, wie gesagt, wird rendemäßig darauf zugegriffen mit NEW, wird
Speicherplatz alloziert.
Hier, wie gesagt, wächst es im Prinzip nach unten und wieder nach
oben.
Das heißt, der Pegel sinkt.
Das heißt, mehr Speicher ist belegt und der steigt.
Das heißt, wieder weniger.
Also genau andersherum wie beim Wasserstand.
So, jetzt können Sie sich vorstellen, also das ist hier gerade diese
Animation.
Hier sehen Sie, hier wächst jetzt noch was drauf auf dem Pegel und
kann dann zum Beispiel größer werden, wenn immer weiter verschachtelt
Methoden aufgerufen werden und dann baut es sich auch immer wieder ab.
Wie gesagt, für uns nehmen wir jetzt eine vereinfachte, abstrakte
Sicht auf den Speicher ein.
Wir sagen, wir haben also zwischen 0 und der maximalen Adresse unseren
Speicherbereich.
Ich indiziere diese Speicherzellen und dann teilt sich das so auf.
Ich habe einen gewissen Teil, da ist mein Programm untergebracht, der
Bytecode.
Das ist das Programm, die Instruktionen, die Sie ausführen sollen.
Bis zur Stelle Maxbrock.
Das Programm ist natürlich konstant groß.
Das wirkt nicht zur Laufzeit größer oder kleiner.
Ein paar Sonderfälle klammere ich jetzt dabei mal aus.
So, dann haben Sie Konstanten.
Das sind Variablen, die aber mit einer konstanten Werte belegt sind.
Sie erinnern sich an dieses Wort Final, was eben dafür sorgt, dass es
sich nicht mehr ändert.
Und die können auch einfach hier mal in den Speicherbereich reingelegt
werden und da braucht man nachher nicht mehr Schreiben drauf
zugegriffen werden.
Und dann gibt es eben diesen ganzen großen Teil von Daten und dieser
Datenbereich, der teilt sich jetzt auch in diese Variablen hier, die
im Keller liegen und eben die Halde, die wir eingangs besprochen
hatten.
Und das ist jetzt erstmal wichtig, dass Sie also diese beiden Begriffe
kennen.
Halde und Stapelspeicher, wo hier die Programmvariablen liegen.
Und was hier alles liegt, werden wir uns nachher noch im Detail
anschauen.
Bevor wir aber jetzt uns das im Detail anschauen, müssen wir noch eine
andere Sache klären, nämlich wie springe ich eigentlich in einem
Programm.
Ja, klar, zum Methodenaufruf muss ich nicht nur Daten mehr merken, ich
muss auch springen.
Dazu ist es wichtig, dass Sie wissen, es gibt einen sogenannten
Befehlszähler.
Der gehört nicht in den Speicherbereich, das ist ein Register in Ihrem
Prozessor, also das Teil Ihres Prozessors.
Und der zeigt dahin, wo ist der nächste Befehl, der abgearbeitet
werden muss.
Und der wird im Prinzip bei sequenzieller Ausführung, also wenn ich
mein Programm schrittweise durchgehe, immer weiter erhöht.
Er zeigt immer auf die nächste Zeile, die ich ausführen muss oder den
nächsten Befehl, den ich ausführen muss.
Und irgendwann mal bin ich halt am Ende.
Gut, jetzt ist es aber so, Programmieren wäre natürlich langweilig,
wenn ich keine Schleifen hätte, wenn ich keine Ifs hätte.
Das heißt, hin und wieder muss ich natürlich mal springen.
Das heißt, mit anderen Worten den Befehlszähler manipulieren.
Und diese Manipulation, die durch eine Zuweisung an den Befehlszähler
passiert, das ist genau der Sprung.
Wenn ich also eine neue Adresse dem Befehlszähler zuweise, dann
springe ich in dem Programm.
Das kann nach vorne sein, wenn ich also einen höheren Wert als ich
üblicherweise habe.
Also als der Befehlszähler einen Sprung nach vorne hatte, dann
überspringe ich einen Teil.
Das wäre zum Beispiel bei einem If.
Wenn die Bedingung falsch ist, dann überspringe ich den If-Teil.
Oder aber ich kann natürlich auch nach hinten springen.
Das ist dann, wenn ich eine Schleife ausführe, dass ich auch so Dinge
wiederholt ausführe.
Und es gibt letzten Endes in der Maschinensprache, auch im Java
-Bytecode, zwei Arten von Sprüngen.
Einmal den sogenannten unbedingten Sprung.
Das heißt, wann immer ich hier hinkomme, springe ich an einen Ziel.
Oder aber ich habe sogenannte bedingte Sprünge.
Das sind die, die ich nutze, um eben Weil-Schleifen, Vorschleifen und
eben die Ifs zu realisieren, wo nur bei einer Bedingung gesprungen
wird.
Und da habe ich noch die Aufrufe von Unterprogrammen, wenn ich
irgendwo hinspringe.
Und dann gibt es auch eine Rückkehradresse.
Das ist im Stand die Adresse nach diesem Sprung.
Also das heißt, hiermit kehrt man auch wieder zurück und geht dann
hier nach weiter.
Wohingegen bei einem Go-To merke ich mir nicht.
Da springe ich einfach dahin und dann läuft es da weiter.
Und ich weiß nicht mehr, wo ich hin, wo ich eigentlich zurück müsste.
Das gibt es hier nicht.
So, was wahrscheinlich den meisten jetzt schon klar ist.
Ich zeige es trotzdem nochmal.
Ich kann eigentlich mit diesen bedingten Sprüngen die ganzen
klassischen Sachen der strukturierten Programmierung nachbilden.
Kontrollflussstrukturen, die ich habe.
Einmal ein If.
Also wenn ich z.B.
sage, hier gibt es eine Bedingung und wenn die Bedingung wahr ist,
dann führe ich eben diesen Teil S aus und U führe ich immer aus.
Aber S eben nur, wenn das wahr ist.
Das funktioniert dann so, dass man hier diese Variable 1 lädt in den
Prozessor, dann die Equals-Routine durchführt und dann sagt, ich
springe zum End hier hin, wenn das nicht der Fall ist.
Und ansonsten führe ich jetzt eben das aus.
Und das ist eben ein bedingter Sprung, der in dem Fall also nur, wenn
das ungleich ist, springt.
Und damit überspringe ich dann eben diesen Teil von S, falls eben
diese Bedingung nicht wahr ist.
Das ist genau die Semantik von dem If.
Das ist also, wenn das wahr ist, führe ich das S aus.
Das baut man sich eben damit nach.
Ein bisschen komplizierter, aber nicht viel komplizierter, ist die
Variante jetzt mit If, Then, Else, wo ich entweder den S oder den T
-Teil ausführe.
Hier wieder überprüfe ich das Ganze auf Gleichheit.
Ich gehe zum Then-Teil, das ist da, wo S ist.
Wenn das Ganze gleich ist, also wenn es gleich ist, soll ja auch S
ausgeführt werden.
Das ist hier der Fall.
Und wenn das ungleich ist, das ist also genau dann, wenn es hier nicht
gesprungen wird, dann gehe ich in den T-Teil rein und springe dann
unbedingt hier zum Ende, zum U.
Warum hier ein unbedingter Sprung?
Naja, ist ja klar, wenn ich hier das T ausführe, dann darf ich
natürlich nicht noch das S ausführen.
Jetzt können Sie sich sagen, warum ist das gerade so?
Hätte man ja auch genau andersherum machen können.
Hätte man, ja, richtig.
Aber Sie sehen, hier wird davon ausgegangen, dass ich, wenn ich S
ausführe, gleich immer zum U übergehen kann.
Und ja, ich habe jetzt im Prinzip damit das so formuliert, dass ich
sage, in den meisten Fällen wird ein S ausgeführt, dann gehe ich
gleich zum U über und hier brauche ich dann auch keinen Sprung mehr
dazwischen.
Aber es ist klar, Sie könnten mit einer Negation die Sprungziele genau
vertauschen mit dem S.
Wenn ich eine Schleife habe, nehmen wir jetzt erstmal die Weil
-Schleife, also wo die Bedingung im Kopf steht, deswegen heißt es die
kopfgesteuerte Schleife.
Auch das passt ja relativ gut.
Ich mache den Vergleich, wieder beladen von Variable A auf
Gleichheitsvergleichen.
Wenn das nicht der Fall ist, springe ich zum Ende hier hin und
ansonsten führe ich den Schleifenrumpf auf und springe wieder nach
oben.
Und das ist auch ein unbedingter Sprung und hier prüfe ich dann wieder
die Bedingung.
Das ist genau die Semantik, dass ich S so lange ausführe, wie das hier
gilt.
Ich habe jetzt im Wesentlichen bei der Programmverwaltung, sind wir
mal wieder zurück bei unserem Speicher,
drei relevante Basisadressen.
Einmal MINVAR, das ist die Anfangsadresse des Speicherbereichs, wo die
statischen Programmvariablen liegen.
Dann im Prinzip den Pegel, der mir sagt, wie weit geht eigentlich mein
Keller runter.
Und dann den sogenannten Umgebungszeiger, der zeigt auf den aktuellen
Block hier, was ist quasi meine, wie wir gleich sehen, relevante
Schachtel, also wo bin ich gerade bei meiner Aufruf-Hierarchie.
Im Prinzip addiere ich dann immer eine relative Adresse auf so eine
Adresse drauf oder auf die Umgebungsvariable und hier soll nur gesagt
werden, ja das geht jetzt in meiner Maschinensprache.
Wenn ich jetzt diesen Teil der Programmvariante mir anschaue, der eben
zwischen MINVAR und dem Pegel liegt, dann beinhalte der sogenannte
Schachtel.
Der Schachtel wird immer bei der Verschachtelung vom Programm auch
angelegt, also wenn sie wieder einen Aufruf machen.
Und was ist da drin?
Ich hatte es schon eingangs erwähnt, lokale Variablen,
Rückkehradresse, Parameterwerte und Rückgabewert.
Und das sieht dann grafisch jetzt so aus.
Sie haben im Prinzip im Keller immer aufeinander diese Schachteln
liegen.
Die Parameterrückkehradresse, lokale Variablen.
Ich habe jetzt bei den Parametern auch noch die Rückgabe-Typen mit
reingemogelt.
Das ist nicht prinzipiell noch unterschiedlich, sie sind hier mit
dabei.
Und dann sehen Sie, im Prinzip gibt es den Kellerpegel, den
Umgebungszeiger, der sagt, ja von da aus sind sie aufgerufen worden,
daher muss man zurückspringen.
Und dann eben aufeinander geschachtelt diese Schachteln.
Und warum eben mehrere?
Naja, wie gesagt, wenn ein Unterprogramm aufruft, dass wieder ein
Weiterer aufruft und so weiter.
Pro Aufruf wird so eine Schachtel angelegt.
Und was passiert jetzt, wenn ich eine Methode aufrufe?
Erstmal muss ich den Kellerpegel erhöhen.
Ich will ja eine neue Schachtel anlegen, also muss der wandern.
Jetzt sehen Sie schon, ich gehe nochmal gerade zurück, wenn dieser
Pegel dahin wandert, wo mein minimaler Hieb ist, dann habe ich
natürlich ein Problem.
Dann würde im Prinzip der Keller in den Hieb, in die Halde
reinwachsen, das darf nicht sein.
Das würde auch die Java Virtual Machine das Ganze abbrechen und sagen
eben, dass sie die maximale Schachtelungstiefe erreicht haben.
Was Sie dann machen können ist, die Java Virtual Machine mit dem
Parameter starten, dass Sie den Hieb kleiner machen.
Dann können Sie natürlich beim Pegel etwas weiter wachsen, aber
prinzipiell haben Sie mal irgendwann das Problem, wenn Sie zum
Beispiel in der Rekursion eine zu hohe Rekursionstiefe haben, kann es
natürlich sein, dass Ihr Keller zu groß wird.
Da sorgt aber die Java Virtual Machine dazu, dass im Prinzip das nicht
hier reinwächst und dann die Daten überschreibt, sondern es wird
einfach abgebrochen.
Sie erhöhen den Kellerpegel, nehmen wir mal an, das geht noch, ich
habe genug Platz.
Dann lesen Sie die aktuellen Parameter aus, das heißt die Parameter,
die Ihnen beim Methodenaufruf übergeben werden.
Setzen die Rücksprungadresse ein, das ist die Adresse, von wo Sie
aufrufen, beziehungsweise natürlich ein Befehl danach, weil sonst
würden Sie ja immer wieder beim Zurückkehren wieder neu springen, das
wollen Sie natürlich nicht, also klar, Sie gehen eine Adresse weiter.
Dann setzen Sie den Umgebungszeiger auf die Schachtel, damit klar ist,
das ist die aktuelle Schachtel und dann führen Sie im Prinzip die goes
up Befehle auf, also den Methoden auf, den Sprung an, die erste
Adresse in der Methode.
Und das sieht grafisch dann so aus, Sie erhöhen den Pegel, bauen
Parameter ein, Rückkehradresse und die Speicherplatz für die lokalen
Variablen.
Die werden dann initialisiert gemäß der Java-Vorgaben und dann können
Sie dann aufarbeiten.
Der Pegel wird hier hingesetzt und das ist dann die Umgebung.
Also hier wird die Schachtel mit adressiert.
Und so sieht das eben in den Schachteln aus.
Und wenn Sie die Methode verlassen, wird der Umgebungszeiger, wird der
Kellerpegel wieder hochgesetzt.
Das ist also nicht mehr eine gültige Schachtel.
Der Umgebungszeiger wird wieder hierhin zurückgesetzt auf diese
Schachtel und damit ist die Schachtel quasi, die steht zwar noch für
sich da, die wird jetzt nicht mit null überschrieben, aber im Prinzip
ist es klar, das ist nicht mehr die aktuelle Schachtel, sondern die
aktuelle Schachtel ist jetzt hier oben drüber.
Und dann wird dahin auch zurückgesetzt.
Die entscheidende Botschaft von diesem technischen Teil der Vorlesung
ist, Sie bauen im Prinzip diese Werte immer auf, auf dem Keller und es
geht wieder zurück.
Das ist aber nicht der Speicherbereich, wo Sie mit new Sachen
allozieren.
Mit new allozieren Sie Sachen auf der Halde.
Das sind also die zwei Speicherbereiche und wie gesagt heap und stack,
das sind die Begriffe, die Sie eben kennen sollten an der Stelle.
Und das wird im Prinzip auch vorausgesetzt jetzt dann im
Sommersemester und deswegen wollte ich es Ihnen noch sagen.
Ich hatte es vorher immer mal hin und wieder auf der Tonspur, aber ich
denke mal, es ist besser, wenn man das einmal auch grafisch gesehen
hat.
So, dann wäre ich jetzt mit diesem Teil fertig.
Es gibt, vielleicht noch so als Veranstaltungshinweis, Vorlesungen
auch, die Sie später noch bekommen werden im Studium.
Eine heißt Programmierparadigmen, wo wir das Thema Übersetzerbau, also
wie schreibe ich Programme, Compiler, die eben ein
Hochsprachenprogramm übersetzen in Maschinencode oder Bytecode
systematisch behandeln.
Das ist schon etwas länger.
Und dann gibt es natürlich auch noch Übersetzerbau als eigene
Vorlesung, wo es nur um dieses Thema geht.
Und fortgeschrittene Objektorientierung, wo auch nochmal ein bisschen
erklärt wird, wie das speziell für objektorientierte Programme geht.
Also Sie sehen, da kann man Vorlesungen drüber füllen.
Das sind nur die Basics, wie gesagt, die Sie noch für das nächste
Semester brauchen.
Das ist insbesondere wichtig für diejenigen von Ihnen, die
Informationswirtschaft studieren und sich entschieden haben, Software
Technik zu nehmen statt technischer Informatik.
Natürlich sage ich, Software Technik ist eine tolle Entscheidung.
Nur klar, meine Kollegen aus der technischen Informatik würden das
anders sehen.
Insofern will ich das jetzt gar nicht weiter bewerten.
Nein, aber diejenigen von Ihnen, die eben nicht mehr technische
Informatik hören werden, für die ist das quasi bewusst auch nochmal
hier eingebaut, weil das ist das Wissen, was sie quasi aus der
technischen Informatik brauchen fürs weitere Studium, um auch die
weiteren Sachen in der Software Technik zu verstehen.
Diejenigen von Ihnen, die Informationswirtschaft studieren und dann
technische Informatik hören, die werden das ohnehin nochmal
ausführlicher kennenlernen.
Und diejenigen, die Informatik studieren sowieso, weil die natürlich
auch die technische Informatik im Pflichtprogramm haben.
So, und zwar noch so ein paar Hinweise zu Best Practices beim
Programmieren.
Also, wenn Sie so wollen, den letzten Segen, den ich Ihnen jetzt auf
den Weg gebe.
Und zwar kommt das aus einem Buch.
Practical Java Programming Language Guide.
Das ist relativ alt, das Buch.
Läuft aber trotzdem in der Bibliothek noch rum.
Es gibt auch noch ein paar neuere Auflagen.
Es hat sich eigentlich nichts geändert seitdem.
Es ist immer noch eine klassische Denkfeile, wo man reinstürzt und
deswegen hier so ein paar Beispiele rausgenommen.
Sie sehen aber, in dem Buch steht viel mehr.
Ich habe die aus meiner Sicht so ein bisschen als Block
zusammengehörenden und wichtigsten Teile rausgepickt.
Aber wie gesagt, wenn Sie da reinschauen, gibt es viele interessante
weitere Dinge.
Also, erstes Prinzip.
Sie sollen nicht anfangen, Gott zu spielen.
Nee, Quatsch.
Selber Instance of aufzurufen.
Und zwar in den meisten Fällen, wenn Sie abprüfen wollen, ob eine
Klasse, ob ein Objekt, eine Instanz einer bestimmten Klasse ist, dann
sollten Sie sich überlegen, ob das wirklich Sinn macht.
Und zwar, sehr oft können Sie die Verzweigung, also das If, Instance
of, ersetzen durch dynamische Bindung und Polymorphie.
Schauen wir uns das Beispiel hier an.
Sie sagen, ich habe eine Schnittstelle Employee, also einen
Angestellten, der hat ein Gehalt, einen Salary und ich habe einen
Manager, der ist jetzt eine Implementierung von Employee und der hat
ein bestimmtes Gehalt und das wird jetzt hier zurückgegeben.
So, und dann habe ich einen Programmierer, das ist auch ein Employee,
der hat auch ein bestimmtes Gehalt.
Sie sehen, dass das Beispiel ist hier von Informatikern gemacht.
Der Programmierer verdient natürlich mehr und er bekommt auch noch
einen Bonus, weil er Programmierer ist.
Und das ist ja so schwer, kann nicht jeder.
So, und jetzt ist es so, der gibt natürlich auch mit seinen Salary
zurück hier und aber er hat noch diese Bonusroutine, das ist neu dazu,
wo er den Bonus zurückgibt.
Erstmal denken wir, ist ja alles prima.
Ich habe hier Variablen, die sind auch Final, das heißt also
Gehaltserhöhungen sind erstmal jetzt nicht drin.
Aber der entscheidende Punkt ist, ich habe tatsächlich hier auch
Routinen, die das Ganze zurückgeben.
Also auch gekapselt, das Ganze ist Private.
Sieht also erstmal alles gar nicht so schlimm aus.
Hier ist auch erstmal auch gar kein Drama dabei, aber wenn ich jetzt
sage, ich habe ein anderes Programm, das berechnet mir jetzt quasi die
Gehaltszahlungen von der Firma.
Dann gibt es hier Calculate Payroll, also ich erstelle eine
Gehaltszahlung.
Das nimmt als Parameter ein Employee, fragt das Salary ab, muss aber
jetzt natürlich noch daran denken, falls das ein Programmierer ist,
dann muss ich ja den Bonus noch dazurechnen, den der bekommt.
Insofern ist es eigentlich relativ naheliegend, dass man denkt, naja
gut, jetzt muss ich das eben hier tatsächlich machen, dass ich hier
mal Instance Off nehme, weil ich ja weiß, da gibt es ja diesen
Sonderfall von den Programmierern, die eben noch zusätzlichen Bonus
haben, den die Manager nicht haben.
Warum ist das jetzt keine gute Idee?
Was ist schlecht daran?
Ich hatte das schon mal vorher gesagt, Instance Off ist immer blöd,
ich greife da nämlich ein scharfer Laufzeitsystem ab, das kostet Zeit,
aber das ist gar nicht der Hauptgrund.
Der Hauptgrund ist nicht Performance, sondern Wartbarkeit von diesem
Programm.
Stellen Sie sich vor, es gibt nachher eine weitere Implementierung von
Employeen, das ist ja durchaus denkbar.
Es gibt ja noch viele andere Rollen in Unternehmen außer Manager und
Programmierer und jedes Mal muss ich jetzt an der Stelle dran denken,
da muss ich unter Umständen mal schauen, genau was das hier für ein
Employee ist, weil da kann es ja immer wieder eine andere Art der
Gehaltsberechnung geben.
Und das ist immer schlecht, wenn Sie quasi schon wissen, wenn eine
Erweiterung im Programm kommt, gibt es noch eine andere Stelle, wo
hoffentlich einer dran denkt, dass diese Erweiterung auch dort
umgesetzt werden muss.
Grundweg ins Übel der Programmierung.
Sie sollten immer schauen, dass immer nur irgendwann an einer Stelle
was erweitert werden muss und dann nicht noch eben im Hinterkopf einer
haben muss, dass das an anderen Stellen auch noch angepasst werden
muss.
Und das ist genau bei mir der Fall.
Also im Prinzip jedes Mal, wenn Sie irgendwo eine Implementierung
machen von Employee, müssen Sie immer dran denken, bei der Payroll, da
muss man ja unter Umständen aufpassen, welche genaue Instanz das ist.
Das ist halt nicht gut.
Hier nochmal schriftlich, also wenn Sie eine weitere Klasse
hinzufügen, auch wieder hier Instanz, schlecht.
So, wie macht man es denn jetzt besser?
Besser macht man es dadurch, indem man das eigentlich dem
Laufzeitsystem bei dynamischen Bindung überlässt, was es macht.
Schauen wir uns das mal gerade hier an.
Das Employee Interface bekommt jetzt erstmal noch einen Bonus, erstmal
als Methode dazu und der Manager Bonus setzt sich auf 0, weil der
kriegt halt keinen Bonus.
Beim Programmierer bleibt erstmal alles so wie es ist und jetzt kann
ich hier an der Stelle halt sagen, ich gebe einfach jetzt hier das
Salary und den Bonus zurück bei den Payrollen.
Und das ist erstmal relativ naheliegend, diese Lösung, dass ich dann
so das Interface hochziehe und dass ich jetzt nicht mehr zusätzlich
hier immer noch eine Instance-Off-Sache mache.
Es gibt auch andere Stellen, wo Sie zum Beispiel eine Methode machen,
Compute Salary, hier drin und die dann zum Beispiel diese
Summenbildung übernimmt und dann wird halt eben dynamisch dazu
geguckt, dass auch die richtige immer aufgerufen wird.
Auch dann brauchen Sie keinen Instance-Off.
Aber wie gesagt, immer wenn Sie Instance-Off sehen, sagen wir okay,
brauche ich das wirklich.
Dann gibt es den Unterschied zwischen Gleich, Gleich und Equals.
Wir hatten das schon mal in der Vorlesung vorher, ich erwähne es aber
hier trotzdem nochmal, weil man eben so schnell auf die Nase fällt.
Die Frage, die häufig gestellt werden von Anfängern, was ist
eigentlich der Unterschied zwischen einem Vergleich mit Gleich, Gleich
und einem mit Equals?
Kann ich alles mit Gleich, Gleich vergleichen und wo brauche ich jetzt
gleich doch ein Equals?
Schauen wir uns auch das nächste Beispiel an.
Das ist so ähnlich wie das, was wir auch schon mal hatten.
Sie haben hier zwei Integer-Variablen, die beide den Wert 10 haben.
Sie vergleichen das Ganze mit Gleich, Gleich und Sie haben dann zwei
Integer -Variablen aber als Klassen hier und dann die entsprechenden
Objekte Ia und Ib und jetzt machen Sie hier im Prinzip genau das
Gleiche.
Sie vergleichen das Ganze mit Gleich, Gleich.
Was kommt raus, wenn Sie in der früheren Vorlesung aufgepasst haben,
wissen Sie es schon.
Hier kommt True raus bei dem oberen und unten kommt aber False raus,
wobei die beiden hier eigentlich auch den Wert 10 haben.
Warum ist das so?
Gleich, Gleich vergleicht bei eingebauten Werttypen die Werte,
deswegen heißt es ja auch Werttyp.
Das ist in beiden Fällen 10 beim eingebauten Werttyp Integer, deswegen
kommt hier True raus.
Gleich, Gleich vergleicht bei Objekten, ob sie an derselben
Speicheradresse liegen und das tun sie natürlich nicht, weil hier
zweimal New aufgerufen wurde und es ist natürlich vollkommen klar,
dass dieses New einen anderen Speicherbereich zurückgibt als das da.
Wäre auch schlimmer nicht.
So, das heißt aber mit anderen Worten, diese zwei Integers haben zwar
denselben Wert, nämlich 10, aber sie liegen an einer anderen
Speicheradresse und deswegen gibt dieses Gleich, Gleich hier ein False
zurück, weil sie nämlich an einer unterschiedlichen Speicheradresse
liegen.
Das ist natürlich nicht das unbedingt, was man haben will und genau,
weil das eben so ist, dass man das nicht haben will, muss man hier
eine Equals-Routine implementieren.
So, wenn man sich das jetzt anschaut, das ist im Prinzip erstmal das
Gleiche, nur nochmal in Länge, man gibt sich noch die ganzen Werte aus
und selbst hier, wie jetzt mal der Wert 10 ausgegeben wird, das ist
immer noch False, klar, weil eben wie gesagt hier Speicherbereiche
verglichen werden.
So und das heißt also, Gleich, Gleich ist nicht dasselbe wie Equals.
Das ist das, was ich auf der Tonspur schon gesagt habe.
Das ist nochmal schriftlich eben das Gleich, Gleich verwendet bei
Vergleichen von Objekten eben die Referenzen, also die
Speicheradressen und deswegen ist das False.
So und wie kann ich also jetzt die Werte in den Objekten vergleichen,
weil das war das, was ich eigentlich ursprünglich wollte.
Ich erwartete nicht zwei Objekte vom Typ Integer ab, die beide den
Wert 10 haben, dass es ja auch gleich ist.
Dann brauche ich eben Equals und das macht dann einen sogenannten
semantischen Vergleich.
Das heißt, es schaut sich die Bedeutung an.
Das ist mit dem Wort semantischen Vergleich hier gemeint.
So, Equals ist eine Methode.
Methoden können Sie nur von Objekten aufrufen, nicht von elementaren
eingebauten Wertetypen und deswegen können Sie das nicht für die
normalen primitiven Datentypen wie Integer aufrufen.
Also das ist, weil Sie einfach kein Objekt haben, da kann man auch
keine Methode aufrufen.
Aber wie gesagt, für die Klassen, die wir haben, können wir natürlich
Equals implementieren.
So und jetzt schauen wir uns das nochmal an.
Unser Beispiel, hier machen wir das mit gleich gleich.
Es geht gar nicht anders bei unseren Wertetypen, bei unserem Integer
und Float hier.
Sie sehen hier, das ist ein Int, das ist ein Float.
Wir vergleichen die trotzdem hier.
Und hier unten drunter haben wir einen Integer und einen Float.
Wir geben die Werte aus und das prüfen wir mit Equals aus, was
rauskommt und zwar in beide Richtungen.
Einmal von dem Integer des Equals aufgerufen, einmal von dem Float
aufgerufen.
Schauen wir uns mal an, was da rauskommt.
Interessanterweise kommt hier oben raus, dass das Ganze gleich ist.
Und das stimmt.
10 ist dasselbe wie 10,0.
Einmal nur als Integer und einmal als Float gespeichert.
Das ist also einfach noch.
Interessanterweise ist aber hier in beiden Fällen die Antwort false,
wenn ich das Ganze bei Objekten aufrufe, obwohl ich das sogar mit der
Equals -Routine mache.
Hintergrund ist der, es gibt so eine Richtlinie, wenn zwei Instanzen
nicht von derselben Klasse sind, dann sollte das Ergebnis false sein.
Wir werden am Ende noch mal gleich diskutieren, ob das immer so
sinnvoll ist, aber das ist die Daumenregel und deswegen ergibt es auch
beides mal false.
Zur Erinnerung, Typecast.
Sie erinnern sich an die Vorlesung.
Hier wird eine erweiterte Konvertierung gemacht von den Datentypen
primitiv, nämlich von der 10 auf die 10,0.
Das geht, ich kann immer einen Integer auch in den Float umwandeln,
weil sonst das gar nicht vergleichbar wäre.
Aber das wird hier automatisch gemacht, insofern steht hier dann 10,0
gleich gleich 10,0 und deswegen kommt hier True raus.
Und das, was ich gesagt habe, Objekte unterschiedlicher Klassen sind
nach dieser Daumenregel nie gleich und deswegen kommt jedes Mal false
raus.
Das war jetzt erstmal der Unterschied zwischen gleich, gleich und
Equals, aber wie schreibe ich jetzt ein Equals, wenn ich selber eins
schreiben muss, weil ich eben einen semantischen Vergleich haben will
zwischen meinen Objekten.
Auch hier wieder schauen wir uns mal ein Beispiel an.
So, ich habe einen Golfball und der ist durch ein paar Attribute
Brand, Make, Compression charakterisiert.
Zwei Stringattribute, Compression.
Ich habe hier einen Konstruktor, der ziemlich genau das macht, was man
erwartet.
Der initialisiert die entsprechenden Variablen und dann habe ich im
Prinzip die Getter-Methoden, die das entsprechend zurückgeben.
Auch das ist eigentlich relativ Standard.
Wenn ich jetzt sage, zwei Golfbälle sind gleich, wenn die Attribute
denselben Wert haben, dann... also angenommen, ich lege mir jetzt zwei
Golfbälle an, Brand X, Professional, Kompression 100 und den anderen
Golfball lege ich mir mit demselben an, dann möchte ich im Prinzip,
dass dann hier die Ausgabe True ergibt.
Also hier Ball 1 equals Ball 2.
Und das tut es aber erstmal nicht.
Mist.
Woran liegt es?
Das liegt daran, dass hier tatsächlich zwei Speicheradressen sind und
das Equals habe ich ja selber nicht definiert.
Ich habe das Equals nur von Objekt geerbt.
Und genau an der Stelle macht das Equals auch nichts anderes, als das,
was man defaultmäßig halt machen kann, mit Speicheradressen
vergleichen.
Ich muss also das Equals selber überschreiben.
Also wenn ich also im Prinzip Equals aufrufe, aber nicht selber
definiert habe in meiner Klasse, dann ist es eigentlich dasselbe wie
Objektreferenzen vergleichen und damit keinen semantischen Vergleich.
Das heißt also, Sie müssen es wirklich von Hand überschreiben.
Wenn Sie eine Equals-Routine überschreiben, haben Sie schon mal ein
Problem.
Equals hat immer den Parameter Objekt.
Das ist ein bisschen nervig.
Das liegt aber im Java-Typsystem.
Equals wird in der Objektmethode definiert.
Also wenn man in der Java-Base-Library nachgucken würde, wie es Objekt
definiert, dann sieht man da eben Equals als Parameter mit Objekt.
Weil das ist ja auch das Equals von der Objektklasse.
Das geht auch gar nicht anders in Java.
Im Prinzip würde es ja keinen Sinn machen, wenn ich jetzt in der
Objektklasse bin, was Spezielleres als Objekt zu verlangen als
Parameter von Equals.
Also das ist schon eine sinnvolle Entscheidung.
Jetzt ist aber das Java-Typsystem so gebaut, dass wenn Sie davon
erben, dass Sie eine Klasse A haben, die spezialisiert Extents, also
das Equals, dann erben Sie das Equals-Mitarbeitungerweise immer noch
mit dem Objekt als Parameter.
Und das ist in Java so.
Es gibt andere Programmiersprachen, da ist das anders, aber in Java
ist das halt so gelöst, dass Sie im Prinzip keine Varianz haben von
den Parametertypen der Methoden, die Sie erben.
Was man erwarten würde intuitiv, das ist die sogenannte Kovarianz.
Das heißt also, wenn Sie was erben, dass dann auch analog die
Parameter von den Methoden, die Sie erben, variiert werden.
Hat Java aber nicht.
Ist auch nicht typsicher.
Das kommt in einer späteren Vorlesung mal dran.
Beispiel bei mir, Komponentenbasierte Softwarearchitektur.
Da werden Sie lernen, warum Kovariant-Typsysteme nicht typsicher sind.
In Java, wie gesagt, vermeidet man das alles.
Man sagt einfach, Sie haben immer den Parametertyp, den auch schon die
Oberklasse hatte.
Das heißt, Sie müssen bei jeder Implementierung der Equals-Methode
erstmal nachschauen, habe ich eigentlich den richtigen Typ.
Das ist jetzt die Implementierung der Equals-Methode bei Objekt.
Man macht im Prinzip auch wieder einen Referenzvergleich.
Das ist ja das Allgemeinste, was man machen kann.
Was Schlaueres kann man auch bei Objekten nicht machen.
Aber Sie wollen ja eine eigene Implementierung machen, wo Sie das
überschreiben.
Und schauen wir uns also die Golfball-Klasse an.
Sie definieren jetzt hier Ihr eigenes Equals.
Und wenn Sie den Vergleich von Objekt mit Sys positiv ist, dann kann
man sowieso schon mal True rausgeben.
Also wenn ich mich mit mir selber vergleiche, ist es klar, dass immer
True rauskommt.
Da brauchen wir nicht mehr weitermachen.
So, aber jetzt kommt eigentlich der interessante Fall.
Was ist, wenn dieses Objekt offenbar woanders liegt, als da, wo ich
selber liege?
Und das heißt, irgendwas sein kann, aber irgendwas anderes.
Da muss ich natürlich erstmal abprüfen, ist das nicht die
Nullreferenz.
Nullreferenz heißt, das ist im Speicher gar nicht alloziert.
Also das ist klar, dann sollte ich nicht weiter darauf arbeiten und
insbesondere keine Attribute auslesen usw., weil das natürlich alles
dann Speicherbereiche sind, wo ich nicht zugreifen darf.
Und das andere, was ich mache, und das ist so ein generelles Pattern.
Sie schauen nach, in welcher Klasse bin ich und Sie schauen nach, von
welcher Klasse ist denn dieses Objekt.
Get Class rufen Sie hier auf.
Und nur wenn das gleich ist, machen Sie überhaupt erstmal weiter.
Weil hier oben kann vom Typ-System Ihnen ja irgendwas reinkommen.
Ein Objekt oder jede andere beliebige Ableitung, Spezialisierung von
Objekten kann hier reinkommen.
Das Typ-System lässt das alles zu.
Und wie gesagt, Sie können an der Stelle, wie ich ja eingangs erklärt
hatte, in diesem gar nicht-Varianten-Java-Typ-System auch nichts
anderes hinschreiben.
Jedes Mal, wenn Sie, wie gesagt, auch in einer von Ihnen definierten
Klasse equals hinschreiben, müssen Sie immer auf den Parameter vom Typ
-Objekt gefasst sein.
Das ist eben keine Co-Variante-Vererbung hier.
Lange Rede, kurzer Sinn.
Also Sie haben das abgeprüft.
Habe ich die richtige Klasse hier?
Wenn ja, kann ich also mal anfangen jetzt tatsächlich das zu
vergleichen, was ich vergleichen will.
Nämlich, ob die ganzen Attribute gleich sind.
Wenn die Attribute gleich sind, sage ich ja, return true.
Die Objekte sind semantisch gleich, weil sie haben dieselben
Attributwerte.
Wenn nicht, also wenn ich hier nicht mit return true rausspringe, dann
mache ich return false.
So, und jetzt endlich passiert das, was ich eigentlich wollte, dass
nämlich tatsächlich zwei Golfbälle, auch wenn es zwei sind, die aber
dieselben Attributwerte haben, tatsächlich bei dem Programm ball1
equals ball2 ausgibt.
Das würde auch mit den Strings, wie erwartet, funktionieren.
Ein kleines Problem gibt es aber, wenn Sie statt String, Stringbuffer
verwendet hätten.
Jetzt können Sie fragen, warum soll ich denn das machen?
Wäre jetzt zu weitführend in dieser Vorlesung.
Antwort ist aber im Wesentlichen, Stringbuffer ist performancemäßig in
der Regel viel schneller als String.
Insofern gibt es gute Gründe, gar nicht String zu nehmen, sondern
Stringbuffer.
Wenn ich das jetzt mache, könnte ich auch meinen, na gut, ist
eigentlich nicht so schlimm.
Hier habe ich aber wieder das Problem bei meiner Equals-Methode, wenn
ich jetzt den Vergleich ausgebe, dass wieder rauskommt, ball1 does not
equal ball2.
Und das liegt daran, weil die Equals-Methode bei Stringbuffer nicht
eine semantische Analyse macht, sondern leider wieder nur vergleicht
auf Referenzen.
Und wenn ich dann zwei Stringbuffer habe, selbst wenn die dieselben
Wert haben, kommt dann wieder beim Equals-Aufruf, dummerweise, raus,
dass es unterschiedlich ist.
Also nicht das, was ich will.
So, was gibt es jetzt für Lösungen für das Problem?
Erste Lösung ist ganz banal.
Na ja, gut, das Problem habe ich mir eingehandelt, weil ich statt
String, Stringbuffer verwendet habe.
Wenn ich jetzt also doch wieder String nehme, ist ja die Welt wieder
in Ordnung.
Stimmt, nur halt, wie gesagt, performance-mäßig nicht so doll.
So, andere Lösungsidee.
Ich schreibe mir eine neue Equals-Methode für Golfball, sodass sie die
Stringbuffer -Objekte erstmal zu String-Objekten konvertiert.
Auf denen kann ich ja das Equals aufrufen, weil String eingebaute
Datentypen sind und damit habe ich eine semantische Analyse
automatisch und bekomme dann mit dem richtigen Ergebnis raus.
Das wäre dann im Prinzip die Variante.
Ich rufe hier immer toString auf und mache wieder die String
-Vergleiche mit Equals hier.
Also dieser Equals hier, das ist nicht mehr der Equals von
Stringbuffer, sondern das ist das Equals von String.
Das klappt, kommt auch genau das raus, was man erwartet.
Aber es wird jedes Mal ein neues String-Objekt erzeugt und das ist
sehr, sehr zeitaufwendig.
Alleine schon immer, wenn New im Hintergrund aufgerufen wird, um den
neuen String zu erzeugen, das ist performance-mäßig die Hölle.
Also nächste Lösungsidee.
Ich schreibe eine eigene Stringbuffer-Klasse, die aber jetzt endlich
mal ein gescheites Equals hat, nicht wie dieses eingebaute
Stringbuffer.
Genau, das ist ein bisschen mühsam, aber im Prinzip... also z.B.
myStringbuffer, das ist eben der Stringbuffer mit der gescheiten
Equals -Methode.
Das ist jetzt hier die Lösung und das geht auch.
Jetzt schauen wir uns gleich den Code an.
Also Sie haben hier myStringbuffer.
Von Stringbuffer selber können Sie nicht erben, sondern Sie können ihn
nur nutzen, weil es als final deklariert war, aber das macht auch Sinn
in der Java-Implementierung.
Also Sie haben jetzt hier myStringbuffer.
Sie erben erstmal von gar nichts, aber Sie nutzen jetzt hier einen
Stringbuffer.
Und erinnern Sie sich an die Hinweise, die ich letztes Mal in der
Vorlesung gegeben hatte.
Überlegen Sie sich immer erst mal, ob Sie wirklich vererben wollen.
Meistens tut es auch eine Komposition viel besser und das ist auch
hier der Fall.
Und es ist auch bewusst schon verboten, dass Sie von Stringbuffer
erben.
Also müssen Sie hier eine Komposition nutzen.
Sie nutzen also hier Ihren Stringbuffer, haben jetzt hier Ihren
Konstruktor, der das macht, was Sie erwarten.
Das setzt also den Stringbuffer auf den richtigen Wert hier, auf den
String.
So, und jetzt kommen wir zum spannenden Teil.
Sie schreiben hier die Equals-Routine.
Erstmal bekommen Sie ein beliebiges Objekt, wie ich eingangs sagte.
Das ist so, das können Sie nicht ändern.
Das ist nervig, aber Sie müssen immer damit leben, dass Sie erstmal
irgendein Objekt bekommen und Sie wissen gar nichts über das.
Erstmal, das ist hier das schnellste performance-mäßig.
Ich schaue, ob ich mit mir selber aufgerufen wurde.
Wenn das der Fall ist, gebe ich True zurück.
Ansonsten wieder übliches Spielchen wie eben.
Wenn es nicht null ist, dann sprich ich kann überhaupt darauf
zugreifen.
Und dann, dass auch von derselben Klasse eine Instanz ist, weil sonst
würde ein Vergleich eh keinen Sinn machen, kann ich mir jetzt
anschauen, ob das tatsächlich zeichenweise gleich ist.
Vielleicht hier noch, je macht man sich zunutze, dass tatsächlich in
Java es fest definiert ist, dass dieser Teil von dem und, links, als
erster ausgeführt wird und der hier als zweiter.
Was wäre, wenn das nicht so wäre?
Dann könnten Sie natürlich Pech haben und hier wird erstmal Objekt
getClass aufgerufen und Objekt ist aber trotzdem noch null.
Das geht nicht, man speichert Zugriffsfehler.
Aber in Java können Sie sich darauf verlassen, wenn Sie das so rum
hinschreiben, wird auch das zuerst abgeprüft.
Wenn das Objekt gleich null wäre, dann würde es gar nicht zu diesem
Aufruf kommen.
So ist die Semantik von dem und zum Glück in Java definiert und das
Ganze würde schon gleich Falls geben und nur wenn das Objekt definiert
ist, also ungleich null ist, dann wird auch dieser Teil noch
ausgewertet von dem und.
Aber das ist nur in Java so definiert.
In C ist es offengelassen.
Also in C würde ich so was mich nicht trauen hinzuschreiben, das hängt
schon vom Compiler ab, ob es klappt oder nicht.
Aber hier in Java, die Welt ist in Ordnung.
Hier nach diesem If weiß ich eigentlich überhaupt erst, dass das
Objekt, was ich habe, tatsächlich auch ein MyStringBuffer ist und
jetzt kann ich anfangen, das Ganze zeichnenweise durchzugehen.
Also wenn die Längen schon mal unterschiedlich sind, dann ist es klar,
brauche ich mir den Inhalt nicht angucken, dann werden sie auch nicht
gleich sein können.
Wenn aber die Längen gleich sind, dann gehe ich quasi jetzt hier das
Ganze zeichnenweise durch und schaue halt, ist es gleich.
Wenn es ungleich ist, sage ich False.
Und ansonsten, wenn ich alles verglichen habe und kein False war, dann
ist es offenbar zeichnenweise gleich, den ganzen Inhalt lang und dann
gebe ich True.
Hierzu brauche ich noch eine kleine Hilfsroutine, CharacterAdd, der
mir quasi so eine Art Arrayartigen Zugriff auf einen StringBuffer
erlaubt.
Das habe ich hier implementiert, wo ich dann das Element von meinem
StringBuffer bekomme.
Was ich jetzt hier mache, ich habe eine eigene Hilfsroutine für einen
StringBuffer definiert in meiner Klasse MyStringBuffer und die
vergleiche ich einfach zeichenweise.
Warum so viel Aufwand?
Naja, ich spare mir, das Ganze in String zu konvertieren, was wie
gesagt eine sehr teure Operation ist.
Und hier sieht man übrigens mal so einen Klassiker wieder.
Ich schreibe zwar viel mehr Code hin, ist aber schneller.
Also bloß, weil es kurz ist, heißt es noch lange nicht, dass es
schnell ist.
So und jetzt kann ich wieder das mit meinem Golfball machen, wie ich
es eigentlich erwarte.
Ich habe jetzt hier meinen StringBuffer, MyStringBuffer eingebaut und
ich habe jetzt hier die Equals-Routine für jetzt Golfbälle und nutze
dann hier an der Stelle das Equals von MyStringBuffer, was ich eben
implementiert hatte.
Und eigentlich genauso wieder wie eben bei den Strings.
Das hat jetzt den Vorteil, es kommt das richtige Ergebnis beim
Vergleich raus.
Also es wird ein richtiges semantisches Equals durchgeführt.
Aber ich habe ein StringBuffer verwendet, was eben performance-mäßig
schlauer ist, als String zu nehmen.
Es gibt noch eine andere Variante, die man machen könnte.
Ich könnte natürlich sagen, ich nehme nicht eine Equals-Routine von
dem MyStringBuffer, sondern ich verwende eine eigene Compare-Klasse,
die dann im Prinzip die StringBuffer auch alle macht.
Das kann ich machen.
Damit spare ich mir einen MyStringBuffer noch als eigene Klasse zu
implementieren.
Aber wie gesagt, es ist an sich nicht so elegant, auch wenn Sie gesagt
haben, es sparen eine Klasse zu machen.
Wenn Sie die Equals-Routine in der Golfklasse eine Compare-Routine
aufrufen und dann wird in Zukunft vielleicht doch wieder ein String
-Objekt verwendet, dann muss man sich wieder diese Equals-Routine
anschauen.
Und so ist die Equals-Routine, die ich geschrieben habe, von dem
Golfball eigentlich relativ naheliegend, weil sie ruft nämlich immer
Equals auf und nicht irgendwas anderes.
Und das heißt, sie ist eigentlich leichter wartbar, auch wenn Sie
jetzt, wie gesagt, den Sauerapfel gebissen haben, Ihre MyStringBuffer
-Routine zu schreiben.
Der Code ist aber trotzdem leichter wartbar, auch wenn er jetzt
vielleicht etwas größer geworden ist.
Wenn Sie jetzt so getrickst hätten mit einer Compare-Routine, wer
weiß, ob das bei der nächsten Evolutionsschritt von Golfball nicht
doch irgendwie dann negativ gewesen wäre, weil dann müssen Sie
unterscheiden, muss ich jetzt irgendwelche Attribute Compare aufrufen,
für andere muss ich jetzt wieder Equals aufrufen, alles unsauber.
So, das nächste Prinzip kümmert sich auch um Equals und zwar auch so
ein paar Fallen, die man da reinkapseln kann.
Und zwar, Sie sehen, es kann ziemlich viel schiefgehen, wenn man
Equals implementiert.
Und es ist nämlich die Frage, brauchen Sie es überhaupt?
Sie sollten nicht standardmäßig, wenn Sie eine neue Klasse anlegen,
immer ein Equals schreiben oder es auch immer weglassen, sondern Sie
sollten sich genau diese entscheidende Frage stellen.
Nur wenn ich sie logisch, also semantisch miteinander vergleichen
will, dann brauche ich eine Equals-Routine.
Ich lasse die Equals-Routine immer weg, sprich ich erbe die von
Objekt, die Referenzvergleiche macht, wenn ich sagen will, wenn zwei
Referenzen ungleich sind, dann ist das auch schon ein Nicht-Equals.
Das kann ja auch häufig der Fall sein, dass Sie sagen, wenn ich zwei
Objekte habe, egal wie die Attributwerte sind, das sind zwei Objekte
und damit ist es nicht Equal.
Und ehrlich gesagt, das Problem, was Sie haben, ist, das hängt
manchmal vom Anwendungskontext ab.
Und das ist so ein bisschen nervig, weil Sie, wenn Sie die Klasse
schreiben, da soll die ja möglichst oft einsetzbar sein.
Das heißt, Sie wissen nicht, ob die Klasse immer nur auf
Objektreferenzgleichheit oder auf Attributgleichheit geprüft werden
muss.
Sprich also, ob Sie eine Equals-Routine brauchen oder nicht.
Und das ist eine der grundlegenden Entwurfsentscheidungen, die Sie
machen, wenn Sie eine Klasse nicht selber definieren, dass Sie sich
selber überlegen müssen, ohne leider zu wissen, wie die Klasse nachher
möglicherweise verwendet wird.
Was heißt Equals an der Stelle?
So, dann ist natürlich eine spannende Frage, was ist denn eigentlich
jetzt, wenn ich erbe?
Was mache ich denn eigentlich, wenn ich erbe mit meiner Equals
-Routine?
Und da gibt es den Hinweis, man sollte Superpunkt Equals verwenden,
also das Equals der Oberklasse.
Und jetzt ist eine Sache, die nicht ganz banal ist.
Können eigentlich Objekte der abgeleiteten Klasse, also der Instanzen
der Unterklasse, sinnvollerweise mit Instanzen der Oberklasse
verglichen werden?
Hier gibt es, wie ich eben sagte, in Java den allgemeinen Hinweis,
eher nein.
Also, da sollte immer False zurückkommen.
Aber es macht natürlich nicht immer Sinn.
Das ist eigentlich nur eine Daumenregel, dass man sagt, wenn zwei
Klassen von unterschiedlichen Instanzen sind, dann sollte immer False
rauskommen.
Man könnte ja schon sagen, ich habe eine Klasse und eine Unterklasse
davon.
Trotzdem aber möchte ich die Instanzen vergleichen können, weil zum
Beispiel gar keine neuen Attribute hinzugekommen sind.
Werden wir nachher noch darauf eingehen.
Schauen wir uns mal hier diesen Code an.
Ich habe wieder Golfball, aber jetzt erbe ich davon.
Ich füge weitere Attribute hinzu.
Two-Piece und Three-Piece.
Semantik ist jetzt den Nicht-Golfspieler nicht klar, aber macht
nichts.
Was Sie machen, Sie haben jetzt erstmal hier wieder einen vernünftigen
Konstruktoraufruf.
Sie bekommen die ganzen Parameter mit.
Sie rufen den Konstruktor der Oberklasse auf, um diese Sachen
mit zu übergeben und richtig zu initialisieren.
Und dann haben Sie noch diesen Construction.
Construction kann eben Two-Piece oder Three-Piece sein, also
zweiteilig oder dreiteilig.
Das wird hier in diesem Attribut gespeichert, Ball Construction.
Sie haben, wenn man so will, den Getter dafür, auch das ist korrekt.
So und jetzt implementieren Sie Equals von der Unterklasse.
Erstmal nach Schema F, wie Sie das kennen.
Sie bekommen hier eben, wie gesagt, wieder Parameter vom Typ Objekt.
Die prüfen erst mal danach, ob sie selber sind.
Dann muss natürlich Truhs zurückgegeben werden.
Ansonsten auch das, was wir eben auch gesehen hatten.
Sie schauen nach, ist das Ganze ungleich null.
Wenn ja, dann prüfe ich noch nach, ob es überhaupt Instanz von
derselben Klasse ist.
Und dann schaue ich mir nach, was hier rauskommt.
Das ist erstmal so gesehen schon mal ganz gut, weil ich prüfe damit
auch tatsächlich nach.
Habe ich zum Beispiel zweimal Instanzen von MyGolfBall oder habe ich
zum Beispiel einmal GolfBall und MyGolfBall, dann würde aber hier das
fehlschlagen, dieses Gleichheit hier.
Und Sie würden schon mal sicherstellen, dass Sie keine Instanz der
Unterklasse mit einer Instanz der Oberklasse vergleichen und dann Truh
rauskommt.
Also das schließen Sie aus.
Aber wo liegt der Fehler?
Sieht Ihnen einer, wo ist der Fehler dieser Equals-Routine?
Also gut, ich zeige es Ihnen.
Interessanterweise werden hier ja nur die Construction nachher
überprüft.
Die Attribute der Oberklasse, diese drei Felder, Maker, Brand und
Compression, die haben wir ganz vergessen zu überprüfen.
Das heißt, die könnten zwei Golfbälle als semantisch gleich
zurückgegeben werden, obwohl sie an drei Attributen differieren.
Das ist sicherlich nicht das, was man will.
Das heißt mit anderen Worten, das war keine schlaue Idee, hier das
Equals so zu schreiben, weil ich halt eben ganz vergesse, noch nach
den Attributen der Oberklasse zu schauen.
Wenn ich jetzt hier so einen Code anschaue von den Golfbällen und ich
jetzt hier diese zwei Golfbälle im Prinzip unterschiedliche
Attributwerte habe, jetzt suche ich gerade genau hier, die Compression
unterscheidet sich.
Trotzdem würde hier Ball1 equals Ball2 rauskommen, weil ganz vergessen
wurde, die Compression zu vergleichen, hier in dieser Implementierung
von uns.
Wie geht man damit um?
Ich muss letzten Endes natürlich es schaffen, auch den Equals
aufzurufen von der Oberklasse und genau das mache ich, indem ich
nämlich hier unser klassisches Muster erweitere.
Also erstmal prüfe ich ab, ob es 0 ist in der Equals-Implementierung.
Wenn das nicht 0 ist, dann prüfe ich nach, ob ich überhaupt über die
richtigen gemeinsamen Klassen rede, von denen ich eine Instanz bin.
Auch wenn das der Fall ist, dann rufe ich noch die Equals-Methode von
der Oberklasse auf.
Und das sorgt erstmal dafür, dass das tatsächlich auch dann auch die
anderen Attribute vergleicht.
Aber, aber Vorsicht.
Eine Sache ist ganz wichtig.
Sie müssen natürlich wissen, wie diese Equals-Routine implementiert
ist.
Und zwar, man könnte das Ganze noch kürzer hinschreiben.
Wenn Sie das nämlich so verkürzt hinschreiben, hier in dieser Lösung,
super -equals-object.
Und man sagt also, nur wenn das die Attribute der Oberklasse gleich
sind, dann vergleiche ich meine.
Und da könnte man ja geneigt sein, das wie gesagt so verkürzt
hinzuschreiben.
Dann könnten Sie natürlich Pech haben, dass zum Beispiel die
Oberklasse gar kein Equals hat und damit das geerbt hat von Object.
Dann rufen Sie Object-Equals ab.
Das gibt natürlich immer Falls zurück, weil Sie zwei unterschiedliche
Referenzen haben.
Und damit, selbst wenn das jetzt hier gleich ist und alle Attribute
gleich sind, haben Sie damit die Eigenschaften Ihres semantischen
Equals, nämlich die Attributwerte gerade wieder verloren.
Deswegen ein bisschen aufpassen hier.
Lieber, wie gesagt, die längere Variante machen.
Erstens mal prüfen Sie danach, ob Sie überhaupt in derselben Klasse
sind.
Und dann müssen Sie immer noch wissen, gibt es überhaupt ein
sinnvolles, semantisches Equals in der Oberklasse?
Sonst würde ich es nicht aufrufen.
Also auch das können Sie nicht immer schreiben, sondern Sie müssen
natürlich wissen, was macht das Equals in der Oberklasse?
Ist das schon ein semantisches Equals?
Weil sonst haben Sie natürlich die Eigenschaft dann auch hier selber
kaputt gemacht.
So, aber an sich ist das die richtige Implementierung.
Jetzt, Sie prüfen erstmal nach, ist es ungleich null?
Ist es überhaupt ein Instanz von der richtigen Klasse?
Und dann schauen Sie nach, sind erstmal die Attribute semantisch
gleich in der Oberklasse?
Wenn das der Fall ist, kann ich noch die zusätzlichen Attribute,
nämlich Construction, in der Unterklasse abprüfen.
Und wenn das alles dann tatsächlich gleich ist, dann sage ich ja,
True, jetzt sind die beiden Objekte tatsächlich semantisch gleich.
Wenn ich jetzt sage, ich habe noch eine weitere Ebene meiner
Vererbungshierarchie, z.B.
zwischen Golfball und MyGolfball jetzt eingefügt, was passiert denn
dann?
Weil MyGolfball SuperEquals aufruft, ruft er natürlich von der next
höheren Ebene, also von der neu eingefügten Ebene, jetzt das Equals
auf und alles funktioniert so, wie es soll.
Insofern ist die Daumenregel, sobald eine Basisklasse die Equals
-Methode implementiert, in Klammern aber nur dann, sollten Sie auch
SuperEquals aufrufen, weil nämlich dann sich um die Attribute der
Oberklasse gekümmert wird.
Aber wichtig hier, nur wenn die Basisklasse natürlich Equals
implementiert, weil wie ich eben gesagt habe, wenn das nicht der Fall
ist, dann haben Sie automatisch im Equals geerbt von Objekt in der
Oberklasse und das macht eben keinen semantischen Vergleich, sondern
nur einen Vergleich der Referenzen.
Und wenn die Basisklasse Java.Object ist, das heißt, Sie haben gar
nicht dahingeschrieben, dass es davon erbt, aber alles erbt hat in
Java davon, dann sollten Sie natürlich auch nicht Super.Equals
aufrufen, weil dann rufen Sie ja auch wieder nur den Referenzvergleich
auf.
So, Verwirrung komplett.
Ich hoffe, dass noch alle dabei sind, aber Fragen bis hierhin.
Alle endgültig erschüttert.
Dann machen wir jetzt noch das Letzte und dann sind wir aber auch
schon durch.
Und zwar haben Sie sich vielleicht jetzt gewundert, warum hatten wir
jetzt GetClass verwendet und nicht InstanceOf.
InstanceOf macht doch dasselbe.
Antwort ist leider nicht.
Das macht schon Sinn, dass ich GetClass verwendet habe.
Also GetClass gibt zurück, was ist der Name der Klasse dieses Objekts.
Von welcher Klasse ist das denn eine Instanz?
Und wenn Sie also in einer Vererbungshierarchie sind, gibt natürlich
GetClass immer die Klasse an, von der Sie eben eine Instanz sind.
Nicht andere Dinge, die auch noch in der Vererbungshierarchie sind.
Bei InstanceOf prüfen Sie, ist das Objekt eine Instanz von einer von
Ihnen vorgegebenen Klasse?
So und das heißt natürlich, Sie wissen, eine Instanz der Unterklasse
kann immer auch die Rolle spielen einer Instanz einer Oberklasse
davon.
Das ist das Substitutionsprinzip.
Kann es natürlich sein, dass wenn Sie eine Unterklasseninstanz haben,
das ist aber natürlich gleichzeitig auch eine Instanz der Oberklasse.
Also Sie gehen im Prinzip den gesamten Baum nach oben, den ganzen
Vererbungsbaum nach oben und jedes Mal ist Ihre Instanz der
Unterklasse auch eben eine Instanz einer der Oberklassen.
Und damit ist natürlich InstanceOf was anderes, weil nämlich
InstanceOf kann für, wenn wir unser Beispiel eben nehmen oder ein
anderes Beispiel jetzt von Employee, Programmer und so weiter.
Wenn Sie sich vorstellen, da gibt es eine Vererbungshierarchie
dazwischen, dann ist im Prinzip das Unterste in der
Vererbungshierarchie immer auch eine Instanz der Oberklasse.
Aber das ist eben bei GetClass nicht der Fall.
Und das ist genau der Unterschied, warum wir GetClass hier verwenden.
Und weil, wie gesagt, GetClass eben dafür sorgt, dass wir tatsächlich
immer exakt die Klasse haben, von der ich auch eine Instanz bin und
eben nicht eine der Oberklassen.
Manchmal, und das hatten wir ja eben schon mal angefangen zu
diskutieren, macht es vielleicht doch Sinn, dass ich eine Instanz der
Unterklasse auch vergleichen kann mit einer Instanz der Oberklasse.
Das ist aber nur dann sinnvollerweise der Fall, wenn ich nur das
Verhalten ändere, aber nicht Attribute hinzufüge.
Wenn ich Attribute hinzufüge, dann ist klar, dann kann eigentlich eine
Instanz der Unterklasse nicht mehr gleich sein, sondern eine Instanz
der Oberklasse, weil die Oberklasse hat das Attribut ja gar nicht.
Wenn ich aber jetzt gar keine, beim Erben was mache, ohne also die
Attribute hinzuzufügen, das heißt also nur Methoden überschreibe, dann
kann es sinnvoll sein, wie wir jetzt gleich sehen, dass tatsächlich
auch InstanceOf mal genutzt wird in Equals.
Und zwar, wenn ich will, dass abgeleitete Klassen von mir, also wenn
ich jetzt eine Oberklasse mit einer abgeleiteten Klasse von mir, ihre
Instanzen mit meinen Instanzen vergleichen können sollen, dann
verwende ich in meiner Implementierung der Oberklasse InstanceOf,
sehen wir gleich ein Beispiel.
Und dann darf die abgeleitete Klasse die Equals-Methode gar nicht
implementieren.
Schauen wir uns das mal an.
Ist das jetzt ein sehr pathologischer Fall?
Nö, ist es eigentlich nicht.
Also nehmen wir mal an, ich habe hier eine Klasse Auto und die hat
jetzt zwei Attribute, der Hersteller und das Herstellungsjahr und es
gibt eine Methode zum Fahren dieses Autos.
Ich sage, ich habe aber eine schlauere Methode, um das Auto zu fahren
und deswegen leite ich jetzt myClass ab.
Hat aber alles andere so.
Wir sehen, das ist ein Beispiel dafür.
Ich ändere das Verhalten durch Überschreiben einer Methode, das macht
Sinn auch jetzt hier zu erben, aber ich füge gar keine Attribute
hinzu.
Und klar, das Equals ist, dass ich die beiden Attribute miteinander
vergleiche und dasselbe gilt ja auch hier für diese Klasse.
Wenn ich das jetzt zum verallgemeinerten Beispiel, ich habe eine
Basisklasse und eine abgeleitete Klasse und in der Basisklasse prüfe
ich jetzt nach, ob Object Instance of Base ist.
Dann ist es so, dass wenn ich nachher hier bei dem Object auch ein
abgeleitetes Objekt von Base habe, von Derived, dann ist es natürlich
immer noch eine Instance auch von der Base-Klasse, weil, wie gesagt,
das ist gerade die Eigenschaft von dem Instance-Offer-Operator, dass
eine Instanz der Unterklasse auch die Rolle spielen kann eben für eine
Instanz der Oberklasse und damit tatsächlich ist eben dieses Objekt
auch eine Instanz der Oberklasse, selbst wenn es von Derived kommt.
Und das heißt, ich kann also jetzt hier den Code anschauen, ich habe
mir eine Base-Klasse besorgen und eine Derived-Klasse und jetzt d
equals b aufrufen und das liefert true.
Und wenn ich das andersherum aufrufe, b, also Basisklasse equals d,
kommt als Ergebnis auch true raus.
Warum?
Genau, weil nämlich hier das der Fall ist, wenn ich dieses Objekt
aufrufe, wenn dieses Objekt eine Instanz der Unterklasse ist, dann ist
es immer noch eine Instanz auch von der Oberklasse und damit kommt
eben hier auch true raus.
Und das ist natürlich eine Symmetrie, die man erwartet.
Der Gleichheits-Operator ist natürlich symmetrisch und das sollte auch
vollkommen egal sein, ob ich d equals b mache oder b equals d.
Aber wie gesagt, das ist jetzt nur der Spezialfall.
Ich habe beim Erben keine weiteren Attribute hinzugefügt.
Wenn Sie beim Erben aber Attribute hinzufügen, dann sollte da immer
false rauskommen.
Symmetrieeigenschaften ist gegeben.
Der Haken an der Sache, und das ist ein bisschen unangenehm, ist, Sie
müssen schon bei der Implementierung der Oberklasse, nämlich bei dem
Implementieren von dem equals von der Oberklasse, sich Gedanken
machen, wie wird denn das mal später mit dem Vergleich mit Instanzen
von Unterklassen sein.
Wenn die vergleichbar sein sollen, schreibe ich nämlich jetzt hier
InstanceOf hin.
Der Haken ist, Sie wissen aber ja natürlich, wenn Sie die Oberklasse
implementieren, noch gar nicht, ob die Leute, die mal später erben von
ihrer Klasse, da Attribute hinzufügen oder nicht.
Und das ist so ein bisschen blöd an der ganzen Geschichte jetzt.
Im Prinzip müssen Sie hier eine Entwurfsentscheidung treffen, schreibe
ich nämlich InstanceOf oder GetClass hin, ohne dass Sie wissen, was
andere Leute, die von Ihnen erben, eigentlich machen beim Erben.
Überschreiben die nur Verhalten, dann war es richtig InstanceOf
hinzuschreiben.
Oder aber, werden die auch Attribute hinzufügen, dann hätten Sie mal
besser GetClass hier hingeschrieben.
Aber wissen Sie in dem Augenblick ja nicht.
Und das ist halt ein Dilemma, in dem man leider ist in Java.
So, kommen wir zur Zusammenfassung.
In diesem Fall, wenn ich das also will, dass die Unterklassen
verglichen werden können, muss ich bei der Basisklasse die Equals
-Methode implementieren und zwar mit InstanceOf und in der Unterklasse
gar nichts machen.
Dann erbe ich einfach davon und rufe damit auch das Richtige auf.
Das habe ich jetzt hier nicht ausgeführt im Beispiel.
Wenn beide Equals-Methoden implementieren, dann ist diese Symmetrie
-Eigenschaft nicht mehr gegeben.
Das will ich aber jetzt zum Beispiel gar nicht machen.
Und wenn die abgeleitete Klasse Equals implementiert, die Basisklassen
jedoch nicht, da kommen auch unter Umständen unterschiedliche Sachen
raus, wenn der eine Objektreferenzen macht und der andere, wenn man
sich das Ganze vergleicht.
Ich habe Ihnen als Beispiel nur die richtigen Fälle hier gegeben.
Also wenn Sie wollen, dass die Instanzen in der Unterklasse
vergleichbar sind, verwenden Sie in der Oberklasse InstanceOf.
Dummerweise müssen Sie es da auch schon festlegen und implementieren
Sie in der Unterklasse nichts.
Alles andere ist falsch.
So, damit würde ich jetzt mit diesen schlauen Hinweisen zum Java
-Implementieren belassen.
Würde Ihnen aber gerne noch mal ganz schnell einen Ausblick geben, was
kommt.
Zwar, was Sie jetzt in diesem Semester gelernt haben, ist das, was man
so gängigerweise Programmieren im Kleinen nennt.
Sie haben Kontrollfluss, Strukturen kennen gelernt, Sie haben
Datentypen kennengelernt.
Reicht das jetzt auch aus, um große Systeme zu bauen?
Und auf der einen Seite so, ja, im Prinzip schon, weil auch große
Systeme werden mit Java zum Beispiel gecodet und natürlich
programmiert zeilenweise.
Insofern ist das, was Sie gelernt haben, natürlich die Basis dafür,
große Systeme zu bauen.
Aber wenn Sie große Systeme bauen, ist es eben leider doch nicht
dasselbe, wie wenn Sie nur Ihren kleinen Übungsprogramm beschreiben.
Und vielleicht klassische Analogie dafür.
Stellen Sie sich vor, Sie bauen in Ihrem Garten ein Gartenhaus.
Das machen Sie ganz toll und Sie schnitzen und Sie schreinern.
Und das ist wunderschön und ganz individuell.
Und alle, die kommen, sagen, wow, was ist das für ein tolles
Gartenhaus.
Und jetzt kommt einer der Auftraggeber zu Ihnen und sagt, Sie haben so
toll Ihr Gartenhaus gebaut.
Bitte bauen Sie mal hier mein Bürohochhaus hin.
Und Sie fangen an, mit denselben Techniken, wie Sie gerade das
Gartenhaus gebaut haben, das Bürohochhaus zu bauen.
Dann wird Ihnen relativ schnell schon intuitiv klar werden, auch wenn
Sie nicht Bauingenieurwesen studieren, das wird so wohl nichts werden.
Weil natürlich so spätestens ab dem dritten Stockwerk ist das
natürlich mit der Konstruktion, mit dem Holz und so, so ein bisschen
wackelig.
An Aufzüge haben Sie auch nicht nachgedacht, Brandschutze auch nicht
und all solche Sachen.
So, lange Rede, kurzer Sinn.
Es ist natürlich schon ein Skalierungseffekt dazwischen, ob ich kleine
Softwareprogramme schreibe oder große Softwareprogramme.
Und die Hauptskalierungseffekte liegen da drin.
Solche Software muss gut wartbar sein.
Ich habe Ihnen schon eine Menge Hinweise dafür gegeben, aber da gibt
es noch viel, viel mehr.
Sie haben in der Regel ein sehr, sehr aufwendiges Zusammenspiel von
Komponenten, von Subsystemen, Teilen, die schon da sind, Teile, die
Sie neu da hinzufügen, Dinge, wo Sie interoperabel sein müssen, wo Sie
Daten austauschen müssen, wo Sie kompatibel bleiben müssen und so
weiter und so fort.
Weil Ihre Software auch nicht auf der grünen Wiese entsteht, wie Ihr
Übungsprogramm, sondern halt sich irgendwo in eine sehr komplexe
Softwarelandschaft einfügen muss in einem Unternehmen.
Und schlussendlich, Sie entwickeln die Software ja nicht alleine,
sondern im Team.
Das heißt, Sie müssen kommunizieren, Sie müssen Entwürfe weitergeben
und so weiter.
Das ist, wenn Sie selber Software schreiben, für sich relativ einfach,
weil wenn Sie eine Schnittstelle einfügen, dann wissen Sie, aha, so
nutze ich die Schnittstelle und so muss ich sie implementieren, weil
Sie sind dieselbe Person.
In dem Augenblick, wo Sie in einem Team arbeiten und der eine sagt,
ich implementiere die Schnittstelle und der andere sagt, ich nutze
diese Schnittstelle, ist klar, da muss man eine ganze Menge
kommunizieren, um überhaupt die eigentliche Bedeutung der
Schnittstelle klar zu machen.
Solcher Dinge mehr.
Wie skaliert quasi Ihr Kenntnis von der Entwicklung kleiner Systeme
zur ingenieurmäßigen Entwicklung großer Systeme?
Genau diesen Schritt machen Sie in der Vorlesung Softwaretechnik 1,
die jetzt im Sommersemester bei Ihnen kommt, die Kollege Tichy liest.
Hier erstmal nur so die Definition.
Was heißt eigentlich Software Engineering?
Software Engineering.
Das ist in der Tat ein großes Versprechen, Softwareentwicklung zu der
Ingenieurswissenschaft zu machen.
Wenn man ehrlich ist, damals als dieser Begriff Software Engineering
geprägt wurde, vor 40 Jahren, war das eher eine Zielrichtung, nicht,
dass wir das schon erreicht hätten.
Und selbst so ist es heute auch noch.
Wenn wir uns Softwaretechniker vergleichen mit anderen Ingenieuren,
Bauingenieure, Maschinenbauer, Elektrotechniker, dann machen wir
leider leider immer noch sehr viel Try and Error.
Wir entwickeln einfach mal was und schauen dann nachträglich, ob es
noch passt.
Durchtesten oder durch den Betrieb beim Kunden.
Wenn Sie das mit anderen Dingen machen, Flugzeuge, Brücken, also ist
nicht so gut.
Also das heißt in anderen Worten, dieses Abschätzen von den
Auswirkungen von Entwurfsentscheidungen, das ist eigentlich das
Kernelement jeder guten Ingenieursdisziplin, dass Sie in der Lage
sind, bevor Sie etwas realisieren, schon in der Lage sind zu
verstehen, wie wirkt sich eine Entwurfsentscheidung aus auf die
Qualität des nachher erst entstellten Produktes.
Das ist eine Sache, wo eben wir hier auch in Karlsruhe dran forschen,
mehr Ingenieurwesen in die Software Technik reinzubringen.
Aber wie gesagt, das ist im Prinzip genau der Kern einer
Ingenieurswissenschaft und wie gesagt, wir versuchen das hier in
Karlsruhe auch hinzubekommen, forschungsmäßig.
Dann gibt es auch bei Java noch vieles, was wir nicht besprochen
haben.
Also auch dieser Einführungskurs konnte nicht alles behandeln.
Einmal das ganze Thema Nebenläufigkeit Paralleles Programmieren.
Das kommt bei Programmierparadigmen dran, bei Ihnen im Pflichtprogramm
und es gibt auch eine Veranstaltung Parallelrechner-Programmierung
hier in Karlsruhe, wo Sie das auch noch mal genauer lernen.
Dann entfernte Methodenaufrufe haben wir nicht behandelt.
Das ist halt, wenn Sie verteilte Systeme haben, die dann gekoppelt
sind über ein Netzwerk.
Das ist das ganze Gebiet verteilte Systeme, kommt zum Teil in der
Telematik auch dran hier in der Vorlesung.
Entwurfsmuster, Designpatterns, das ist Lösungen für wiederkehrende
Entwurfsprobleme.
Das wird auch in der Software Technik 1 behandelt.
Was wir auch nicht behandelt haben, ist das ganze Thema grafische
Oberflächen.
Wie kann ich GUIs programmieren?
Das ist, wenn man so will, auch ein eigenes Thema, wo es sehr, sehr
viele Werkzeuge, Generatoren, Bibliotheken gibt, haben wir nicht
behandelt.
Java Beans ist eine Möglichkeit dazu, die allerdings zugegebenermaßen
auch langsam in die Jahre gekommen ist.
Dann gibt es Java Security.
Das ganze Thema sicheres Programmieren haben wir nicht behandelt.
Das kommt in der Software Technik 2 dran, was aber im Wahlfach nur
noch ist.
Das wird also nicht jeder von Ihnen unbedingt hören.
Aber Sie sehen, es gibt haufenweise Themen noch, die wir aus diesem
Kern der Anfängervorlesung leider nicht mit reinnehmen konnten, die
wir ausgelassen haben, aber die Sie im Laufe des Studiums eigentlich
noch hören können zumindest mal.
Viel Erfolg im weiteren Studium zu wünschen, natürlich viel Erfolg
auch bei den Abschlussaufgaben und natürlich auch beim Übungsschein
für die, die ihn noch nicht haben.
Vielen Dank.