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, quasi 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 ein paar Folien 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.
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 finden Sie das auch in der Bibliothek.
Und da gibt es einen zweiten Band von der Vorlesung über Informatik,
Objekte und dieses 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.
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 javac 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
hat das eigentlich funktioniert hier, die Geschichte.
Jetzt ist es so, 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 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.
Und dazu Begriffe.
Also es ist so, wir haben uns den Speicher immer bisher in den
Beispielen über die Arrays immer 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 also die
maximale Adresse, die das Programm adressieren kann.
Und dann gibt es ab Min Heap 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, man 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, das taucht Ihnen nachher
mal in der Betriebssystemvorlesung 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 einen möglichst gut passenden Block 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 hin und 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 alles an
planbarem Echtzeitverhalten kaputt macht.
Aber das jetzt erstmal zum Begriff HALDE oder Englisch Heap.
Da gibt es einen anderen Speicherbereich, das ist der Stapelspeicher
oder auch Kellerspeicher.
Im Englischen wird es Stack, von 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 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 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, dass ich das wieder runternehme.
Und dieser Stapelspeicher, das ist der, den wir auch brauchen beim
Methodenaufrufen.
Jetzt sehen wir hier erstmal ein Beispiel, ich habe jetzt eine
Befehlsreihenfolge Push, und nochmal ein Push-B, und dann ein Push-C,
das baut sich quasi als Stapel auf, und ein 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 Unterprogramms.
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.
Irgendwie müssen die ja jetzt übergeben werden an die Methode.
Die müssen ja von dem Code der Methode aus nutzbar sein.
Und auch die kommen hier auf den Stapel dran.
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 bauen peu à peu, immer wieder, brauchen Sie neuen
Platz auf dem Kellerspeicher, wenn Sie nämlich eine neue Methode
aufrufen.
Und wenn zum Beispiel 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 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.
Speicherbereich steht ja 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.
Das können Sie sich vorstellen, 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 MaxProc, das Programm ist natürlich konstant groß, das
wird nicht zur Laufzeit größer oder kleiner.
Ein paar Sonderfälle klammere ich jetzt dabei mal aus.
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 auch 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 auf in diese Variablen
hier, die im Keller liegen und eben die Halde, die wir eingangs
besprochen hatten.
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 uns das aber im Detail anschauen, müssen wir noch eine
andere Sache klären.
Nämlich, wie springe ich eigentlich in einem Programm?
Klar, zum Methodenaufruf muss ich nicht nur Daten, wir 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 ist Teil Ihres Prozessors.
Und der zeigt dahin, wo ist der nächste Befehl, der abgearbeitet
werden muss.
Und der wird bei sequenzieller Ausführung, also wenn ich mein Programm
schrittweise durchgehe, immer weiter erhöht.
Der zeigt immer auf die nächste Befehle, die ich ausführen muss.
Und irgendwann mal bin ich halt am Ende.
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 Zuweisung an den Befehlszähler
passiert, das ist genau der Sprung.
Wenn ich 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 also 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 ein 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 dann 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.
Dann springe ich einfach da hin und dann läuft es da weiter.
Und ich weiß nicht mehr, wo ich hin, 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, ist, 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 zum Beispiel 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 im Prinzip 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, dass ich sage, 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 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.
Ich habe das jetzt im Prinzip damit so formuliert, dass ich sage, in
den meisten Fällen wird dann S ausgeführt, dann gehe ich gleich zum U
über und hier brauche ich dann auch keinen Sprung mehr dazwischen.
Aber ist klar, Sie können 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 hier relativ gut.
Ich mache den Vergleich, wieder laden von Variable A auf
Gleichheitsvergleichen.
Wenn das nicht der Fall ist, springe ich zum Ende hier hin und
ansonsten führe ich die Schleife auf und springe wieder nach oben.
Und das ist auch ein unbedingter Sprung und hier prüfe ich dann wieder
die Bedingungen.
Das ist genau die Semantik, dass ich S so lange ausführe, wie das hier
gilt.
Ich habe jetzt im Wesentlichen bei der Programmverwaltung, das sind
wir 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 meine relevante Schachtel, wo bin ich gerade bei
meiner Aufrufhierarchie.
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 beinhaltet er sogenannte
Schachteln.
Schachtel wird immer bei einer Verschachtelung vom Programm 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 Kellerspeicher immer aufeinander diese
Schachteln liegen.
Die Parameter, Rückkehradresse, lokale Variablen.
Ich habe jetzt bei den Parametern auch noch die Rückgabetypen mit
reingemogelt.
Das finde ich 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, das wieder ein
Weiterer aufruft und so weiter.
Nur 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,
wenn dieser Pegel dahin wandert, wo mein Miniminerheap ist, dann habe
ich natürlich ein Problem.
Dann würde im Prinzip der Keller in den Heap, in die Halde
reinwachsen, das darf nicht sein.
Das würde auch die Java Virtual Machine das Ganze abbrechen und sagen,
dass sie die maximale Schachtelungstiefe erreicht haben.
Was Sie dann machen können, ist die Java Virtual Machine mit einem
Parameter starten, das Sie in den Heap 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 einer 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 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 Go-Sub-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 den Speicherplatz für die lokalen
Variablen.
Die werden dann initialisiert, gemessert, Java-Vorgaben, und dann
können Sie aufarbeiten.
Der Pegel wird hier hingesetzt und das ist dann der Umgebung.
Also hier wird die Schachtel mit adressiert.
Und so sieht das dann in den Schachteln aus.
Und wenn Sie die Methode verlassen, wird der Umgebungszeiger, wird der
Kellerpegel wieder hochgesetzt.
Das ist also dann 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 0 ü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ückgesprungen.
Jetzt gerade auf das letzte Bild, sorry.
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.
Deswegen wollte ich es Ihnen noch sagen.
Ich hatte es vorher immer mal hin und wieder auf der Tonspur.
Aber ich denke, 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,
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 mein O-Sprachenprogramm
ü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 darüber füllen.
Hier, das sind nur die Basics, wie gesagt, die Sie noch bis nächstes
Semester brauchen.
Es ist insbesondere wichtig für diejenigen von Ihnen, die
Informationswirtschaft studieren und sich entschieden haben,
Softwaretechnik zu nehmen statt Technische Informatik.
Natürlich sage ich, Softwaretechnik 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 Softwaretechnik 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, wie 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 Off 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
Off, 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
einem 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, das Beispiel ist hier von Informatikern gemacht.
Der Programmierer verdient natürlich mehr.
Und er bekommt auch noch einen Bonus, weil er Programmierer ist.
Das ist ja so schwer, kann nicht jeder.
So, und jetzt ist es so, der gibt natürlich auch mit seinen Salary
zurück.
Aber er hat noch diese Bonusroutine, das neu dazu, wo er den Bonus
zurückgibt.
Erstmal denken wir uns 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
Gehaltszahlungen.
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ätzlich einen Bonus haben, den die
Manager nicht haben.
Warum ist das jetzt keine gute Idee?
Was ist schlecht daran?
Ich hatte das schonmal vorher gesagt, Instance Off ist überblö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, ups, 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 an einer Stelle was
erweitert werden muss und dann nicht noch im Hinterkopf einer haben
muss, dass das an drei anderen Stellen auch noch angepasst werden
muss.
Und das ist genau hier der Fall.
Jedes Mal, wenn Sie irgendwo eine Implementierung machen von einem
Employee, müssen Sie immer daran 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 Instance auf.
Schlecht.
So, wie macht man es denn jetzt besser?
Besser macht man es dadurch, indem man das eigentlich dem
Laufzeitsystem bei dynamischen Bindungen überlässt, was es macht.
Schauen wir uns das mal gerade hier an.
Das Employee Interface bekommt jetzt erstmal noch einen Bonus als
Methode dazu.
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 dem Payroll.
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 okay,
brauche ich das wirklich.
Dann gibt es den Unterschied zwischen Gleich, Gleich und Equals.
Wir hatten das schonmal in der Vorlesung vorher, ich erwähne es aber
hier trotzdem nochmal, weil man eben so schnell auf die Nase fällt.
Also die Frage, die so ist, die häufig gestellt wird 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 schonmal 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.
Obwohl 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 schlimm, wenn nicht.
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
die nämlich an einer unterschiedlichen Speicheradresse liegen.
So, 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 jetzt hier das 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, ist es
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.
Es 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, dass ich zwei Objekte vom Typ Integer habe, 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, 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 jetzt 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 ja 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 Instanz sind, dann
sollte das Ergebnis False sein.
Wir werden am Ende nochmal 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 dem Datentyp
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.
So, 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 lege ich mir jetzt zwei Golfbälle an, Brand X, Professional,
Compression 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 das?
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 machen kann, nämlich Speicheradressen
vergleichen.
Ich muss also das Equals selber überschreiben.
Also wenn ich 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 am 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, also Sie haben eine Klasse, zum Beispiel A, die spezialisiert
Extents, also das Equals, dann erben Sie das Equals mit, aber
dummerweise 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, zum Beispiel bei mir, Komponentenbasierte Softwarearchitektur,
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.
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.
Wenn ich mich mit mir selber vergleiche, ist es klar, dass immer True
rauskommt.
Da brauchen wir nicht mehr weitermachen.
Jetzt kommt 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.
Dann sollte ich nicht weiter darauf arbeiten und insbesondere keine
Attribute von 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 Typsystem Ihnen ja irgendwas reinkommen.
Ein Objekt oder jede andere beliebige Ableitung, Spezialisierung von
Objekten kann hier reinkommen.
Das Typsystem 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-Typsystem 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 Quo-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.
So,
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 mir meinen, na gut, das 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.
Naja 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 performancemäß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.
Also 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, weil 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, zum Beispiel
MyStringBuffer, das ist eben der StringBuffer mit der gescheiten
Equals -Methode, das ist jetzt hier die Lösung.
Und das geht auch.
Und zwar 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
sich an die Hinweise, die ich letztes Mal in der Vorlesung gegeben
hatte.
Überlegen Sie sich immer nach, erstmal, 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.
Der 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,
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 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 Object
.getClass aufgerufen, und Object ist aber trotzdem noch Null.
Das geht nicht, haben wir einen Speicherzugriffsfehler.
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 Faults 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 etwas mich nicht trauen hinzuschreiben, das
hängt nämlich 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 MyString-Buffer ist.
Und jetzt kann ich anfangen, das Ganze zeichenweise 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 zeichenweise durch und schaue halt, ist es gleich.
Wenn es ungleich ist, sage ich Faults.
Und ansonsten, wenn ich alles verglichen habe und kein Faults war,
dann ist es offenbar zeichenweise gleich, den ganzen Inhalt lang.
Und dann gebe ich True.
Hierzu brauche ich noch eine kleine Hilfsroutine, CharacterAdd, der
mir quasi so eine Art Array-artigen Zugriff auf meinen String-Buffer
erlaubt.
Das habe ich hier implementiert, wo ich dann das Element von meinem
String -Buffer bekomme.
Was ich jetzt hier mache, ich habe eine eigene Eclipse-Routine für
einen String-Buffer definiert, in meiner Klasse MyStringBuffer.
Und die vergleiche ich jetzt einfach zeichenweise.
Warum so viel Aufwand?
Naja, ich spare mir, dass das Ganze in den String zu konvertieren,
was, wie gesagt, eine sehr teurere 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.
Und jetzt kann ich wieder das mit meinem Golfball machen, wie ich es
eigentlich erwarte.
Ich habe jetzt hier meinen String-Buffer, 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 einen String-Buffer 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 String-Buffer 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 es
sparen, eine Klasse zu machen.
Weil wenn Sie die Equals-Routine in der Golfklasse, eine Compare
-Routine aufrufen, und dann wird in Zukunft vielleicht dann doch
wieder ein String-Objekt verwendet, dann muss man sich 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
nur ein Equals auf und nicht irgendwas anderes.
Und das heißt, sie ist eigentlich leichter wartbar.
Auch wenn Sie jetzt, wie gesagt, den sauren Apfel 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, wie
gesagt, wer weiß, ob das bei dem 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 reinklapsen kann.
Und zwar, Sie sehen, es kann ziemlich viel schief gehen, wenn man
Equals implementiert, und es ist die Frage, brauchen Sie es überhaupt?
Sie sollten nicht standardmäßig, wenn Sie eine neue Klasse anlegen,
immer ein Equals schreiben oder 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 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 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?
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.
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 drauf eingehen.
Schauen wir uns mal hier diesen Code an.
Ich habe wieder Golfball, aber jetzt erbe ich davon und füge weitere
Attribute hinzu.
Two-Piece und Three-Piece.
Semantik ist jetzt den Nicht-Golfspielern nicht klar, aber macht
nichts.
So, was Sie machen, Sie haben jetzt erstmal hier wieder den
vernünftigen Konstruktoraufruf.
Sie bekommen die ganzen Parameter mit.
Sie rufen den Konstruktor der Oberklasse auf, um diese Sachen 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 dreiteiliger Golfball.
Das wird hier in diesem Attribut gespeichert.
Ball Construction.
Sie haben im Prinzip, wenn man so will, den Getter dafür.
Auch das ist korrekt.
So, und jetzt implementieren Sie die 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 wir erstmal danach, ob sie es selber sind.
Dann muss natürlich True 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 True
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 in 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.
So, 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.
So, und
jetzt, 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 Null ist in der Equals
-Implementierung.
Wenn das nicht Null 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 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, wo 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
Attributwerte 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,
sondern nach, 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.
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 sich
selber kaputt gemacht.
So, aber an sich ist das die richtige Implementierung.
Sie prüfen erst mal nach, ist es ungleich null?
Ist es überhaupt ein Instanz von der richtigen Klasse?
Und dann schauen Sie nach, sind erst mal 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 in meiner
Vererbungshierarchie, da steht jetzt 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 unserer 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 schon 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 Prinzip das 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.link.object ist, das heißt Sie haben gar
nicht dahingeschrieben, dass es davon erbt, aber alles erbt halt in
Java davon, dann sollten Sie natürlich auch nicht Super.Equals
aufrufen, weil dann rufen Sie ja auch wieder nur den
Referenzenvergleich 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.
Also von welcher Klasse ist das 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.
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 wir haben 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.
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 zu einer 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,
ein Equals.
Und zwar, wenn ich will, dass abgeleitete Klassen von mir, also wenn
ich jetzt eine Oberklasse bin und abgeleitete Klassen von mir, ihre
Instanzen mit meinen Instanzen vergleichen können sollen, dann
verwende ich in meiner Implementierung der Oberklasse InstanceOf,
sehen wir gleich am 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, die
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.
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 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 Derive, dann ist es natürlich
immer noch eine Instanz auch von der Base-Klasse, weil wie gesagt, das
ist gerade die Eigenschaft von dem Instance of 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.
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 Base-Klasse 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 Gleichheitsoperator ist natürlich symmetrisch und es 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.
Symmetrie-Eigenschaften ist gegeben.
Der Haken an der Sache und das ist ein bisschen unangenehm, ist, Sie
müssen schon bei der Implementierung der Oberklasse, beim
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 im Augenblick ja nicht.
Und das ist halt, ja, ein Dilemma, in dem man leider ist in Java.
So.
Genau.
Kommen wir zur Zusammenfassung.
In diesem Fall, wenn ich das also will, dass die Unterklassen
verglichen werden kann, 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.
Wenn, das habe ich jetzt hier nicht ausgeführt, im Beispiel, wenn
beide Equals-Methoden implementieren, dann gibt es die Symmetrie
-Eigenschaft nicht mehr gegeben.
Das will ich aber jetzt im Beispiel gar nicht machen.
Und wenn die abgeleitete Klasse Equals implementiert, die Basisklasse
jedoch nicht, dann kommen auch unter Umständen unterschiedliche Sachen
raus, wenn der eine Objektreferenzen macht und der andere semantisch
das Ganze vergleicht.
So, aber wie gesagt, 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.
Ich würde Ihnen aber gerne noch mal ganz schnell einen Ausblick geben,
was kommt.
Was Sie jetzt in diesem Semester gelernt haben, ist das, was man so
gängigerweise Programmieren im Kleinen nennt.
Sie haben Kontrollfluss, Strukturen kennengelernt, 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 zahlenweise.
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 eine 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 denn das für ein tolles
Gartenhaus?
Und jetzt kommt einer der Auftraggeber zu Ihnen und sagt, oh, Sie
haben so toll Ihr Gartenhaus gebaut, bitte bauen Sie mir 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 mit der
Konstruktion, mit dem Holz und 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 steht, wie Ihr
Übungsprogramm, sondern halt sich irgendwo in eine sehr komplexe
Softwarelandschaft einfügen muss in einem Unternehmen.
So, und schlussendlich, Sie entwickeln die Software ja nicht alleine,
sondern im Team.
Das heißt, Sie müssen kommunizieren, Sie müssen Entwurfs in Dänen
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.
Solche 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 ist the application of systematic quantifiable
approaches to the development, operation and maintenance of software.
This is the application of engineering to software.
Und das ist in der Tat ein großes Versprechen.
Software Entwicklung 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, das
ist nicht so gut.
Das heißt mit 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 wir hier auch in Karlsruhe dran forschen, mehr
Ingenieurwesen in die Softwaretechnik reinzubringen.
Aber wie gesagt, das ist im Prinzip genau der Kern einer
Ingenieurswissenschaft und wir versuchen das hier in Karlsruhe auch
hinzubekommen, forschungsmäßig.
Dann gibt es auch bei Java noch vieles, was wir nicht besprochen
haben.
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 nochmal 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 verteilter 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 Softwaretechnik 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 zugegeben ist,
auch langsam etwas in die Jahre gekommen ist.
Dann gibt es Java Security.
Die ganzen Themen sicheres Programmieren haben wir nicht behandelt.
Das kommt in der Softwaretechnik 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 jetzt einfach
aus diesem Kern der Anfängervorlesung leider nicht mit reinnehmen
konnten, die wir rausgelassen 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.