Herzlich willkommen zur Programmieren-Vorlesung.
Bevor ich in den Stoff vom letzten Mal wieder einsteige, kleinen
Veranstaltungshinweis.
Morgen um 18.30 Uhr findet hier für Sie, auch im Audimax, eine
Veranstaltung des Mentorenprogramms der Fakultät statt.
Da sind Sie alle zu herzlich eingeladen.
Morgen, 17.30 Uhr, hier im Audimax.
So, wir waren das letzte Mal stehen geblieben bei Methoden.
Wir hatten uns, vielleicht erinnern Sie sich, so knifflige Sachen
angeschaut wie den folgenden Fall.
Ich habe einen Konstruktoraufruf.
Ich rufe eine Methode auf, das ist diese Shift-Operation auf diesem
Objekt.
Mache dann eine Zuweisung der Objektreferenzen hier und rufe dann auf
dem anderen Objekt das auf und weil die Objektentität nur einmal da
ist, auch wenn zwei Referenzen da sind, tatsächlich addieren sich
diese beiden Operationen auf und das Ergebnis ist tatsächlich hier das
letzte.
Das war das, was wir gemacht hatten.
Und das nur mal zur Erinnerung.
Wir waren dran mit Konstruktoren, wie erzeuge ich Speicherplatz, wie
erzeuge ich Objekte.
Wir waren dran mit Methoden, also wie kann ich bestimmte
Aufrufsequenzen, bestimmte Befehlssequenzen so bündeln, dass ich sie
von verschiedenen Stellen her aufrufen kann, wie eben diese Shift
-Methode.
So, und wo wir jetzt weitermachen, das ist ein Fall, der ist in der
Tat vielleicht nicht so ganz einfach zu verstehen, insofern muss ich
mich ein bisschen konzentrieren.
Und zwar hatten Sie ja gesehen, wir haben bisher Methoden aufgerufen,
immer mithilfe eines Objekts.
Und es ist tatsächlich so, jedes Objekt hat ja für sich seine
Attribute, seine Variablen, die wir deklariert hatten in der Klasse.
Und wenn ich zwei verschiedene Objekte habe, zweimal auch New
aufgerufen habe, also anders als in dem Beispiel, wie wir das eben
gesehen haben, dann ist es natürlich klar, dass die Attribute von dem
einen nichts mit dem Attributen von dem anderen zu tun haben, weil es
sind ja zwei verschiedene Objekte.
Jetzt gibt es aber manchmal Methoden, die machen gar keinen Zugriff
auf die Attribute, weil sie vielleicht einfach nur etwas konvertieren,
wie hier in unserem Beispiel, wo wir sagen, ich habe eine Routine, die
konvertiert einen Celsius-Parameter in Fahrenheit.
Dafür braucht man ja gar keinen Zugriff auf irgendwelche Attribute.
Das ist im Prinzip eine Formel, die hier, wie gesagt, bei der Return
-Anweisung angegeben ist, wie man eben von einer Celsius-Umgabe in
eine Fahrenheit-Umgabe wandelt.
Und tatsächlich kann man diese Art von Methoden auch als sogenannt
statisch deklarieren.
Statisch ist ein ganz furchtbares Wort in der Programmierung, weil es
hat eine ganze Reihe von ganz verschiedenen Bedeutungen.
Hier heißt in dem Falle static, dass es diese Routine
ConvertToFahrenheit als sogenannte Klassenmethode existiert.
Das heißt, es gibt sie nur einmal für eine Klasse und man kann sie
auch tatsächlich aufrufen, ohne dass man vorher ein Objekt erzeugt.
Man braucht das Objekt für den Aufruf nämlich nicht, weil diese
Methode arbeitet auch gar nicht auf irgendwelchen Objektattributen.
Das hatten wir ja eben als Vorbedingung gesagt.
Und das heißt, ich habe so ein bisschen den Vorteil auf die Art und
Weise, dass ich eine Methode aufrufen kann, ohne vorher ein Objekt zu
erzeugen.
Wie gesagt, in den allermeisten Fällen hat man diesen Wunsch gar
nicht, weil die Methoden, die ich aufrufe, müssen auf einem Objekt
arbeiten, sinnvollerweise, wie eben die Shift-Operation.
Einen Klassevektor kann ich nicht shiften.
Ich kann immer nur Objekte der Klassevektor verschieben.
Aber in dem Fall jetzt hier mit unserem Beispiel Convert-to
-Fahrenheit, wäre es ja albern, wenn ich mir vorher ein Objekt erst
erzeuge, um dann eine Convert-to-Fahrenheit-Routine aufzurufen, die
eigentlich gar kein Objekt braucht.
Dann nehme ich diese sogenannten statischen Methoden und dann schreibt
man einfach Static davor.
Static bezieht sich also nicht auf den Rückgabetyp Double, sondern
Static bezieht sich auf die komplette Signatur hier, auf Static,
Double, Convert-to-Fahrenheit, Double Celsius.
Und wie gesagt, dann kann man es eben aufrufen mit Klassenname, Punkt,
Methodenname und dann die Parameter, wie Sie das kennen.
Wie gesagt, der Vorteil ist, Sie müssen eben kein Objekt erzeugen.
Klappt aber natürlich auch nur, wenn Sie kein Objekt brauchen zur
Abarbeitung der Methode, aber das ist ja hier der Fall.
Wenn man das mal so ein bisschen nebeneinander stellt.
Nehmen wir mal an, ich habe meine Klasse Wetterstation.
Da habe ich die Convert-to-Fahrenheit-Routine als Klassenmethode drin.
Und dann würde ich eben aufrufen WeatherStation, also den
Klassennamen, Convert-to-Fahrenheit, 15 Grad Celsius.
Die Objektmethoden, die wir kennengelernt hatten, das Shift, Beispiel
vom letzten Mal, das ist eine Objektmethode.
Und hier habe ich im Prinzip meine Klassendefinition.
Ich habe zwei Attribute, X und Y.
Und ich habe die Shift-Operation, die tatsächlich diese Verschiebe
-Parameter hier nimmt.
Delta X, Delta Y, das aufaddiert auf das X und Y hier von den
Attributen.
Das hier kann einfach keine Klassenmethode werden, weil es braucht
Attribute.
Und die Attribute müssen halt mal instanziert worden sein.
Das klappt nur auf Objekten, deswegen ist das halt auch ein gutes
Beispiel für eine Objektmethode.
Wohingegen hier, wie gesagt, da hat man gar keinen Zugriff auf
Attribute, dann kann man das auch als Klassenmethode machen.
Und man sieht eben beim Aufruf hier, Sie können Shift erst aufrufen,
wenn Sie sich eine Instanz der Klasse erzeugt haben mit einem
Konstruktor, mit dem New-Aufruf hier.
Und dann können Sie darauf P.Shift machen, den hier, wie gesagt,
Objektname, hier der Klassenname.
Das ist der Unterschied beim Aufruf.
Und der Vorteil ist eben, Sie brauchen hier kein New mit irgendwas
machen, sondern Sie können gleich auf die Klasse arbeiten.
So, das ist nochmal quasi schriftlich das, was ich Ihnen gerade auf
der Tonspur gesagt habe zum Nachlesen und Lernen.
Wichtig ist vielleicht noch, gehen Sie sparsam mit Klassenmethoden um.
Also es ist nicht der Stil oder die Erwartung, dass Sie erstmal alles
zu Klassenmethoden machen und dann nachträglich sich noch überlegen,
wie kann ich den Entwurf noch so umbauen, dass ich noch mehr
Klassenmethoden habe oder sowas.
Das ist nicht das Ziel bei dem ganzen Manöver.
Gehen Sie davon aus, das objektorientierte Paradigma hat erstmal die
Annahme, wie ich das in der ersten Vorlesung gesagt habe, ich habe
verschiedene Objekte, die kommunizieren miteinander über
Methodenaufrufe.
Das ist erstmal die Idee, wie Objektorientierung funktioniert.
Und nur in den Ausnahmefällen, wie ich es Ihnen eben gesagt habe, die
haben eine Methode, die problemlos ohne ein Attributzugriff auskommt,
kann man dann halt sagen, zur Bequemlichkeit mache ich das jetzt eben
zu einer Klassenmethode.
Theoretisch müssten Sie das nicht.
Sie könnten es natürlich auch nach wie vor als Objektmethode lassen,
nur das ist dann wie gesagt mit der Erzeugung eines unnötigen Objektes
halt immer ein bisschen umständlich.
So, dann kommen wir zum nächsten Thema.
Und zwar, wenn ich jetzt schon Methoden erzeuge, dann kann es sein,
dass ich da auch nochmal zusätzliche Variablen brauche, die nicht in
den Attributen drin sind.
Und zwar sind das dann sogenannte lokale Variablen.
Lokal, weil sie nur einen im Vergleich zu den Attributen
eingeschränkten Gültigkeitsbereich haben, nämlich den einer Methode.
Und zwar können Sie eben sagen, hier vielleicht erst mal das Beispiel,
ich möchte die Fläche eines Kreises berechnen und Sie erinnern sich
vielleicht in der Schule, das ist Pi mal r², r ist der Radius und das
kann ich natürlich mit Return hingeben.
Natürlich gehen bei Ihnen, weil Sie bei mir Vorlesungen gehört haben,
sofort die Warnleuchten an und sagen, oh, was ist denn das, wenn das
ein Überlauf ist und so weiter.
Interessiert mich jetzt nicht, es geht ja nur um das Beispiel von
lokalen Variablen.
So, und jetzt können Sie auch sagen, was jetzt nicht endlos viel
schlauer ist, aber es ist halt nur mal ein Beispiel für eine lokale
Variable.
Sie sagen, okay, das Quadrat vom Radius, das möchte ich noch in einer
eigenen Variable behalten.
Das heißt, dann deklarieren Sie hier eine lokale Variable, die heißt
radius², auch von Typ Double, das hat das Quadrat des Radius und dann
rechnen Sie halt hier im Folgenden damit.
Jetzt werden Sie zu Recht sagen, schön, ich habe jetzt eine lokale
Variable gesehen, aber so richtig Sinn macht das Manöver für mich
nicht.
Recht haben Sie.
An der Stelle macht es natürlich keinen Sinn, diese Variable
einzuführen, weder von der Verständlichkeit her, weil hier rafft auch
jeder, worum es geht.
Und wenn einer das hier nicht versteht, dann würde er auch das nicht
verstehen.
Und weiterhin kann man auch sagen, warum will ich das jetzt noch als
lokale Variable abspeichern?
Aber, stellen Sie sich mal den generelleren Fall vor, Sie möchten
vielleicht häufiger auf das Quadrat des Radius zugreifen.
Nehmen wir mal an, Sie haben jetzt hier eben nicht sofort das Return
Statement, wo eh die Methodenausführung beendet ist, sondern Sie haben
da vielleicht noch weitere Berechnungen und Sie brauchen häufiger das
Quadrat des Radiuses.
Natürlich können Sie jedes Mal radius mal radius hinschreiben, aber
Sie jedes Mal multiplizieren Sie radius mal radius.
Warum?
Sie können es auch einmal berechnen und dann immer das
Zwischenergebnis quasi mehrfach nutzen.
Dann würde das eben durchaus Sinn machen, hier eine lokale Variable
hinzuschreiben, radius squared, oder eben generell für alle möglichen
Zwischenergebnisse, wo man sagt, die möchte ich halt nur einmal
berechnen und dann halt mehrfach nutzen können.
Das wäre ein sehr starkes Argument für eine lokale Variable.
Manchmal schreibt man aber auch lokale Variablen einfach nur der
besseren Verständlichkeit hin.
Wie gesagt, hier das ist insofern kein super Beispiel, weil radius mal
radius ist genauso verständlich, mindestens wie radius squared, gebe
ich zu.
Aber manchmal ist es vielleicht doch eben etwas komplexer und man
möchte einfach nochmal einen leichten, menschlich gut verständlichen
Begriff einfügen und das kann man eben auch mit einer lokalen Variable
machen.
Ich weiß, die ganzen Performance Cracks von Ihnen sagen, das kostet
aber so endlos viel Speicherplatz und so weiter und so fort.
Ja, klar.
Man kann sich Anwendungen vorstellen, wo das so viel Speicherplatz
kostet, aber hier die paar Byte.
Egal.
Dann könnte man auch auf die Idee kommen und ich hoffe, das hat noch
nie einer von Ihnen nur ansatzweise gewagt, darüber nachzudenken, das
jemals zu tun.
Man könnte natürlich auch eine Parameter nehmen als lokale Variable.
Das geht.
Das wird der Compiler Ihnen nicht verbieten.
Sie können also sagen, ich habe eine Routine foo, für die noch nicht
so Programmierfahnden foo und bar, das sind immer so Standardvariablen
-Namen, die man so ein bisschen als Informatiker im Jargon hat.
Wenn man sagt, das soll irgendwas machen, ist mir vollkommen egal.
Ich brauche jetzt nur mal gerade einen Namen, dann sagt man oft foo
dazu.
So, also ich habe jetzt hier einen Parameter a und ich habe eine
lokale Variable x, das ist a plus 1 und dann weise ich a x² zu.
Spätestens jetzt sollten Sie sich in die Zehennägel zusammenrollen.
Natürlich geht das syntaktisch, aber Achtung, Achtung, a war
eigentlich mal unser Parameter und das hatte hier einen Wert bekommen
von außen.
Der ist jetzt weg, der ist überschrieben, der Wert.
Den können Sie vielleicht noch mühsam zurück ausrechnen, durch Wurzel
ziehen und sowas.
Aber schlecht, das ist schon mal ein Nachteil.
Wenn Sie jetzt ein Parameter überschreiben, wie gesagt, das geht
syntaktisch in Java, das kompiliert auch, das wird auch ausgeführt,
aber der Parameterwert ist dann weg.
Das heißt, wenn Sie danach darauf nochmal zugreifen wollen, keine
Chance.
Der andere Grund, warum das schlechter Stil ist, ist eigentlich nicht
nur so sehr, dass der Wert überschrieben wurde und weg ist, sondern es
ist einfach so, die Verständlichkeit des Programms leidet.
Es ist so, jeder Variablename ist ja nicht nur ein Name für eine
Speicheradresse, das hatte ich Ihnen schon beigebracht, sondern ein
Variablename ist auch immer letzten Endes ein Konzept aus der
Anwendungswelt idealerweise, was über den Namen transportiert wird.
Das wird dann vielleicht mit dem Namen A nicht so richtig klar.
Aber stellen Sie sich mal vor, Sie haben hier Gehalt ohne Abzüge oder
sowas stehen.
Dann ist das relativ verständlich, was das ist.
Wenn Sie das jetzt einfach so mindestens überschreiben, dann ist die
Chance hoch, dass das, was danach die Bedeutung ist, eine andere ist
wie vorher.
Wenn Sie hier einen Parameter haben, der einen Namen hat und dem
Parameter weisen Sie irgendwas zu, dann ist in der Regel die Bedeutung
nicht nur bei Wertflöten gegangen, sondern auch die Bedeutung ist eine
andere danach.
Und das ist einfach schlecht.
Versuchen Sie immer zu sagen, wenn ich Variablen habe, wenn ich lokale
Parameter habe, wenn ich Parameter habe, wenn ich Variablen habe, egal
ob Attribute oder lokale Variablen, dass die immer im ganzen Programm,
da wo sie auftauchen und da wo sie verwendbar sind, immer dieselbe
Bedeutung haben.
Sie sollten nicht anfangen, jetzt einfach mal, oh, aus Gründen der
Speicherplatzoptimierung habe ich einen Bezeichner, der hat einen
nichtssagenden Namen T1, so super, weiß keiner, was das heißt.
Und das kann ich einmal dafür nehmen, dass ich hier irgendwelche
Parameter drin speichere.
Dann nehme ich ihn mal, um ein Kommandozeilenparameter abzurufen.
Dann nehme ich ihn mal, um noch ein Zwischenergebnis zu speichern.
Und dann nehme ich das auch noch, um noch eine Ausgabe mehr
mitzumachen.
Klar, kann man alles machen.
Ist aber im Prinzip, wie gesagt, extrem verwirrend, weil man, wenn man
den Bezeichner sieht, nicht sofort damit eine Bedeutung verbindet.
Und wenn er einen sprechenden Namen sogar hat, ist die Bedeutung grad
unterschiedlich, je nach Stelle im Programm, wo ich unterwegs bin.
Und das ist einfach schlecht wartbare Software.
Sie erinnern sich hoffentlich an meinen wichtigsten Satz, den ich
Ihnen versuche einzuhämmern.
Sie schreiben das Programm nicht für den Compiler, sondern für den
nächsten Menschen, der es lesen muss.
Und das ist einfach, erschwert die Verständlichkeit.
Jetzt können Sie immer sagen, Sie sind ja wohl nah an der Debilität,
wenn Sie das nicht verstehen.
Ja klar, aber wie gesagt, die Komplexität der meisten
Softwareprogramme kommt daher, weil sie groß sind und auf einmal
Millionen Zeilen Code haben, und Sie eben nicht mehr in Ruhe alles
durchgehen können.
So, dann gibt es so ein bisschen die Frage, wie klappt das denn
eigentlich beim Aufruf einer Methode?
Sie erinnern sich noch, da haben wir den Übergang von den sogenannten
formalen Parametern, das sind die, die hingeschrieben wurden, wenn Sie
die Methode definieren, die Sie auch verwenden, quasi dann im
Methodenrumpf und den sogenannten aktuellen Parametern, das sind die
Werte, die Sie beim Aufruf mitgeben, das waren die aktuellen
Parameter.
Und jetzt ist es tatsächlich so, in Java werden alle Sachen erstmal,
der Übergang von den aktuellen Parametern zum formalen wird der
aktuelle Parameter kopiert.
Aber, jetzt erinnern Sie sich an die Sache wieder, Objektreferenz
versus Objektidentität.
Die Objektreferenz wird kopiert, die Objektidentität aber nicht.
Das heißt, und das ist eigentlich erstmal auch das, was Sie wollen,
dass dasselbe Objekt, was Sie außerhalb der Methode haben und dann
angeben als Parameter, auch dasselbe Objekt ist, was Sie innerhalb der
Methode ansprechen.
So weit, so gut.
Das heißt aber, wenn Sie das Objekt innerhalb der Methode manipulieren
und die Methode ist irgendwann beendet, dann ist diese Manipulation
natürlich auch außerhalb der Methode sichtbar.
Oft ist das auch genau das, was man haben will, aber manchmal nicht
immer.
Manchmal würde man es lieber haben, wenn ich quasi bei meinem
Methodenaufruf im Methodenrumpf auf einer Kopie arbeite und eben nicht
auf dem selben Objekt.
Bei normalen eingebauten festen Datentypen in Java, Integer, Long,
haben Sie das.
Da arbeiten Sie immer auf der Kopie.
Das heißt, der aktuelle Parameter, der kann ja auch eine Konstante
sein, der wird aber kopiert in den formalen Parameter.
Sie arbeiten nachher auf den formalen Parameter, das Ergebnis ist weg,
wenn ein Return gemacht wird und Sie wieder zum Aufrufer zurückkehren.
Aber bei Objekten ist es so, Sie übergeben eine Objektreferenz, die
wird auch kopiert.
Java ist da super konsequent.
Alles wird kopiert, nur dummerweise wird die Objektreferenz kopiert,
aber nicht die Objektidentität und damit arbeiten Sie auf dem selben
Objekt.
Und hier sieht man das mal an dem Beispielcode.
Sie haben hier Ihre Routine main.
Sie haben eine Variable x ist gleich 7 und Sie rufen func auf und func
setzt hier den formalen Parameter y auf 5.
Aber hier an der Stelle ist x immer noch 7, weil im Prinzip dieser
Wert von x wurde kopiert in das y rein, y wurde überschrieben, das x
ist aber nach wie vor 7, weil Sie hier auf der Kopie gearbeitet haben.
Aber Achtung, Achtung, Achtung, jetzt rufen Sie das Ganze mit einem
Objekt auf, fu, das ist eine Routine fu, die auf dem formalen
Parameter q das Attribut x auf 5 setzt.
Und jetzt machen Sie hier, Sie erzeugen sich eine Objektreferenz, das
ist das p, Sie erzeugen sich eine Objektidentität hier mit nu, das ist
der Vektor, auf dem dann p zeigt und rufen fu p auf.
Tatsächlich wird die Objektreferenz von p, also die Adresse, die hier
bei nu zurückgekommen ist, kopiert.
Die wird hier kopiert in das q.
Aber dann wird dereferenziert q.x und das heißt im Prinzip wird jetzt
auf die Objektidentität natürlich zugegriffen und weil auf die
Objektidentität zugegriffen wird, wird dasselbe Objekt, nämlich das,
was hier mit nu Vektor erzeugt wurde, tatsächlich manipuliert, das
heißt nach dem Methodenaufruf ist tatsächlich p.x jetzt 5.
Und das erkläre ich deswegen so ausführlich, weil das klingt erstmal
sehr verwirrend.
Warum ist das bei Objekten anders als bei normalen eingebauten
Elementardatentypen?
Die Antwort ist, eigentlich ist es gar nicht anders.
Es wird jedes Mal kopiert, aber die Objektidentität wird nicht
kopiert, sondern nur die Objektreferenz wird kopiert.
Und das heißt mit anderen Worten, dadurch haben sie eben diesen Effekt
in Java, dass die Manipulation von Objekten außerhalb sichtbar ist.
Die Manipulation von Elementardatentypen wie hier Integer ist nicht
außen sichtbar.
Aber es ist im Prinzip konsequent, nur man muss halt einmal drüber
nachdenken.
So, dann nochmal kurz zur Unterscheidung, weil das auch früher
manchmal immer so Fragen waren, die hochkamen.
Was ist denn jetzt der Unterschied zwischen einem Attribut und einer
lokalen Variable?
Die lokale Variable gilt innerhalb einer Methode.
Ich schreibe sie im Prinzip im Methodenrumpf ganz oben hin.
Das Attribut gilt außerhalb der Methoden.
Im Prinzip erstmal sichtbar im Objekt, weil es für die ganze Klasse
halt immer gilt.
Die Lebensdauer, das heißt wann ist es überhaupt als Speicherbereich
alloziert und nutzbar, das ist mit Lebensdauer gemeint, das gilt für
die lokale Variable immer nur während diese Methode abgearbeitet wird.
In dem Augenblick, wo Return aufgerufen wird und sie wieder
zurückkehren in den Kontext des Methodenaufrufs, ist im Prinzip auch
die Lebensdauer beendet der lokalen Variable.
Sie können dann auch nicht mehr darauf zugreifen.
Und das geht auch nicht, weil die Zugänglichkeit, also von wann kann
man überhaupt zugreifen darauf, geht halt immer nur innerhalb einer
Methode und es ist klar, wenn die Methode beendet ist, dann hat man
von Ausdauer gar keine Chance zuzugreifen.
Und man nutzt eben die lokale Variable, um, wie ich gesagt habe,
Zwischenergebnisse zu speichern.
Beim Attribut ist es hingegen so, die Lebensdauer hängt vom Objekt ab.
In dem Augenblick, wo das Objekt mit New erzeugt wurde, der
Konstruktor aufgerufen wurde, gibt es das Attribut als Variable.
Sie können Werte zuweisen, sie können Werte auslesen.
Und in dem Augenblick, wo das Objekt aufhört zu leben, sprich in Java,
der Garbage Collector ist vorbeigekommen, die Speicherbereinigung hat
es einkassiert, weil man gemerkt hat, es gibt gar keine Objektreferenz
mehr auf diese Objektidentität.
Solange ist im Prinzip das Objekt, ist das Attribut da, aber Sie
können natürlich zu Recht sagen, in dem Augenblick, wo keine Referenz
mehr auf die Objektidentität da ist, dann kann man natürlich auch
nicht mehr darauf zugreifen.
Und das stimmt auch.
Im Prinzip ist also auch die Zugänglichkeit immer nur innerhalb der
Methoden der Klasse.
Es sei denn, man hat es nicht gekapselt, dass es von außen zugreifbar
ist.
Aber wie gesagt, das klappt auch nur, solange eine Referenz auf das
Objekt da ist.
In dem Augenblick, wo ich keine Referenz mehr habe, ist vollkommen
egal, ob der Speicherbereiniger da war oder nicht.
Ich kann natürlich nicht mehr zugreifen.
Jetzt einmal tief durchschnaufen, weil jetzt wird es noch wilder.
Jetzt reden wir über Klassenvariablen.
Ich hatte ja eingangs gesagt, wir haben Klassenmethoden.
Das sind die Methoden, die im Prinzip aufrufbar sind bei der Klasse,
ohne dass sie aber auf Attribute der Klasse zugreift.
Was es jetzt zusätzlich noch gibt, sind sogenannte statische
Attribute.
Hier taucht auch wieder dieses Wort Static auf, was wir schon eben bei
den Methoden hatten.
Das sind im Prinzip Attribute, die eben auch nicht für jedes Objekt
neu erzeugt werden, in einer neuen Instanz erzeugt werden, sondern die
wirklich nur einmal da sind für die Klasse.
Deswegen eben Klassenvariable, Klassenattribut, statisches Attribut.
Das ist nur einmal da pro Klasse.
Und auch wenn Sie mehrere Instanzen der Klasse erzeugen, dieses
Attribut ist nur einmal da.
Und im Prinzip, wenn es manipuliert wird, sehen das auch alle.
Es gibt eben nicht verschiedene Instanzen davon.
Wenn Sie sonst ein normales Attribut manipulieren, gilt das natürlich
immer nur innerhalb des Objekts, wo das Attribut gilt.
Wenn Sie ein anderes Objekt haben, können Sie das wieder anders
manipulieren.
Das hat keine Wechselwirkung, weil es wirklich verschiedene Variablen
sind.
Bei den Klassenvariablen ist das aber nicht so.
Die gibt es nur einmal, egal wie viele Instanzen ich habe.
Und das schauen wir uns am besten auch wieder im Beispiel hier an.
Ich habe die Klasse Ball.
Sie erinnern sich an unser Billiardspiel zum Beispiel, wo ich hier
eine Kugel, einen Ball habe.
Und das hat ein normales Attribut, die Größe.
Wir hatten auch andere Attribute.
Die Farbe, das Gewicht, das interessiert uns aber nicht.
Wir haben nur die Größe.
Jetzt habe ich hier eine Klassenvariable, die sagt, die Anzahl der
Bälle, die unterwegs ist.
Das ist Number of Balls.
Die wird am Anfang auf 0 initialisiert.
Und danach kann man im Prinzip im Konstruktor das erhöhen.
Beim Konstruktor setzen Sie die Größe auf den Parameter.
Also dieses Muster, oder man nennt Muster, wenn Sie so programmiernah
sind, wie jetzt hier die Sachen, auch Idiom oft beim Programmieren.
Dieses Idiom haben Sie schon oft gesehen.
Ich habe hier einen Parameter.
Und was Sie auch machen, Sie manipulieren Ihre Klassenvariable.
Das ist okay.
Jedes Objekt darf die Klassenvariable erstmal manipulieren.
Und Sie sagen, jetzt ist offenbar ein Ball neu erzeugt worden.
Also Number of Balls++.
Und jetzt ist es tatsächlich so, wenn Sie jetzt einen neuen Ball
erzeugen, wird nicht das hier ausgeführt.
Das setzen Sie auf 0.
Das wäre jetzt auch ziemlich kontraproduktiv.
Dann wird das auf 0 gesetzt, dann um 1 erhöht, wenn der Konstruktor
ausgeführt wird.
Nein.
Das ist im Prinzip eine Initialisierung, die der Compiler sogar
vornimmt.
Die wird gar nicht zur Laufzeit ausgeführt.
Und das heißt tatsächlich, wenn ich jetzt hier zwei Konstruktoraufrufe
habe für zwei Bälle, Ball B1, Ball B2, und Sie sehen wirklich auch
zwei neue Objektidentitäten, die mit new erzeugt werden, dann wird
tatsächlich die Anzahl der Bälle erhöht und es kommen tatsächlich zwei
raus.
Und was Sie machen können, Sie können einmal das ausgeben, wie Sie das
eben bei den Methoden gehört haben.
Dass Sie nämlich einmal sagen, Klassennahme, Punkt, wie wir das eben
bei der Methodennahme hatten, bei der statischen Methode.
Genauso können Sie auch beim statischen Attribut mit Klassennahme
Punkt Attributnahme zugreifen.
Was Sie auch können, aber was nicht empfohlen wird, Sie können
natürlich auch eines der Objekte nehmen, was Sie ja eh schon haben,
und das natürlich auch nutzen, um die Klassenvariable zu
referenzieren.
Das geht, ist aber vom Stil her, wie soll ich sagen, nicht empfohlen.
Jetzt können Sie ja sagen, warum denn nicht, wenn es doch geht.
Warum ist es dann nicht empfohlen?
Und der Grund, warum das so ein bisschen verpönt ist, ist halt, hier
schreibt man halt B1 Number of Balls.
Man hätte genauso gut B2 Number of Balls schreiben können.
Das ist ja vollkommen egal.
Sie können irgendeine Instanz nehmen, nur der nächste Leser muss sich
Gedanken machen, gab es einen besonderen Grund, warum hier Ball 1
steht und ich sollte das vielleicht nicht ändern.
Oder ich muss mir vielleicht Gedanken machen, wenn ich den dritten
Ball einführe.
Oder aber es hat keinen Grund.
Und allein dieses wieder überlegen, ist das, was hier steht, bewusst
so gemeint oder war das eben zufällig?
Und hier ist es de facto nur zufällig.
Sie hätten genauso gut B2 Number of Balls schreiben können.
Sowas will man vermeiden beim Programmieren, weil es jedes Mal beim
Lesen und beim Verständnis des Programms wieder mal irgendwie
Überlegungen in Gang setzt, die man sich sparen kann.
Und wenn Sie hier einfach Ball Number of Balls schreiben, ist es klar,
es ist vollkommen egal, welches Objekt gemeint ist, es ist eine
Klassenvariable und deswegen schreiben Sie hier die Klasse hin.
Und das ist wie gesagt der Hintergrund von dieser Regel, warum ist das
nicht empfohlen, weil wie gesagt, das hier eine zufällige Auswahl ist
und das muss man sich jedes Mal neu überlegen, wenn man den
Programmtext liest.
So, noch keiner schreiend weggelaufen.
Dann machen wir das nächste, was geht.
In Java beim Thema Methoden.
Hier können Sie überladen.
Überladen klingt natürlich cool, so überladener LKW, überladenes
Flugzeug.
Überladen von Methoden geht auch.
Überladen heißt im Wesentlichen, dass man sagt, ich habe verschiedene
Methoden, die haben aber denselben Namen.
Wenn Sie ein Fremdwort verwenden wollen, weil Sie vielleicht irgendwie
mal bei Ihren Eltern rechtfertigen müssen, warum Sie dieses teure
Studium hier machen, dann können Sie das auch Ad-Hoc-Polymorphie
nennen, aber soll ja nicht komplizierter klingen unter uns, als es
notwendig ist.
Also, Überladen sagt, ich habe um mehrere Methoden, die haben
denselben Namen, weil sie im Prinzip semantisch unheimlich nah sind.
Also sie machen im Prinzip quasi dasselbe.
Deswegen will ich auch nicht permanent neue Namen erzeugen, aber der
Compiler kann nachher unterscheiden, was Sie wirklich meinen durch die
Parameterwerte.
Und schauen wir uns das mal hier an einem Beispiel an.
Wir sind wieder in unserer Klasse Vector2D und Sie haben eine Routine,
die heißt Rotate.
Und die nimmt einen Winkel und rotiert um diesen Winkel den Vektor.
So, und dann haben Sie eine weitere Routine, die rotiert auch,
deswegen nennt man sie naheliegenderweise auch Rotate.
Die nimmt aber tatsächlich einen Vektor, um einen anderen Punkt zu
drehen.
Nämlich um Center.
Und macht dann im Prinzip, ruft dann auch Rotate um den Winkel auf,
aber verschiebt sie erstmal quasi um diesen Punkt Center hier.
Das ist ja auch so von Shift, nehmen wir mal an, das haben wir auch
hier deklariert, wie wir es schon vorher kennengelernt haben.
Jetzt ist es so, beide machen im Prinzip erstmal was ziemlich
ähnliches, rotieren die eine aber wie gesagt um einen Center-Vektor,
der andere halt erstmal ohne das, um sich selber.
Und lange Rede, kurzer Sinn, das ist ein Beispiel jetzt von diesem
Überladen, von dieser Ad-Hoc-Polymorphie, weil Sie sehen, der Name ist
zwar derselbe, Rotate, aber die Parameter sind anders.
Hier habe ich einen Parameter, einen Double, und hier habe ich zwei
Parameter, nämlich einen Vektor 2D und dann einen Double.
Und der Compiler sieht ja, wenn er den Code kompiliert, beim Aufruf,
welche Parameter übergeben Sie.
Und damit ist es dem Compiler möglich, auch wenn Sie zweimal denselben
Namen vergeben haben, zu unterscheiden meinen Sie jetzt diese
Implementierung von Rotate oder diese Variante von Rotate.
Und das ist insofern immer dann möglich, wenn Sie über die
Parameterlisten eindeutig identifizieren können, was ist gemeint,
welche Methode ist eigentlich gemeint hier an der Stelle.
Dann können Sie denselben Namen verwenden, das heißt überladen.
Das geht natürlich nicht, wenn Sie eine weitere Routine Rotate hätten,
die auch einen Vektor nimmt und einen Double.
Selbst wenn Sie die formalen Parameter anders nennen, der Compiler
geht nach den Typen und erkennt eben Vektor und Double.
Und wie gesagt, selbst wenn Sie das jetzt hier nicht Center nennen und
nicht Angle, sondern anders, trotzdem, das klappt dann nicht beim
Überladen.
Das geht immer nach der Typliste, nach der Typbezeichnung.
Und wenn zwei Typbezeichnungen gleich sind, aber die Implementierung
anders, dann kann der Compiler trotzdem nicht unterscheiden beim
Aufruf, was gemeint ist.
Und er wird Ihnen eine Fehlermeldung zurückgeben und sagen, ich kann
nicht unterscheiden, welche Methode ich hier einsetzen soll.
Ich kann den Code nicht kompilieren, das reicht an Information nicht.
Aber wie gesagt, in dem Fall ist es natürlich eindeutig, schon
alleine, weil die Zahl der Parameter anders ist und wie gesagt,
natürlich auch, dass eindeutig von den Typen ist.
Das nur als vielleicht Begründung.
Zur Unterscheidung nimmt der Compiler eben nicht nur die Anzahl der
Parameter, sondern schon auch die Typen.
Das heißt, Sie könnten mehrere Rotate-Routinen haben mit zwei
Parametern, solange die nicht dieselben Typen haben und der Compiler
das eindeutig feststellen kann, welcher Typ beim Aufruf gemeint ist,
kann er das unterscheiden.
Es gibt für die, sag ich mal, Einstein-Hörer hier natürlich auch
Fälle, wo der Compiler das nicht statisch unterscheiden kann.
Wenn Sie zum Beispiel eine Ober- oder Unterklasse haben, wo auch bei
der Unterklasse die Instanz der Ober- oder Unterklasse auftauchen
kann, dann wird der Compiler das aber auch sagen.
Das ist nicht statisch diskriminierbar, dann bekommen Sie auch eine
Fehlermeldung.
Aber für alle anderen, die Typen müssen unterschiedlich sein.
Sich nur im Rückgabetyp zu unterscheiden, das ist nicht möglich.
Es geht tatsächlich über die Parametertypen.
Der Hintergrund ist der, Rückgabetypen können sehr leicht automatisch
konvertiert werden und dadurch ist es dem Compiler auch nicht immer
möglich rauszufinden, nur anhand des Rückgabetyps, was war jetzt hier
wirklich eindeutig gemeint.
Deswegen hat man einfach mal im Sprachstandort das Ganze vereinfacht
und gesagt, es geht immer nur über die Parametertypen.
Auch wenn im Prinzip man sich schon viele Fälle vorstellen kann, wo
man auch über den Rückgabetyp diskriminieren könnte, es ist aber
manchmal eben auch nicht eindeutig feststellbar.
Und wie gesagt, das würde jetzt in dem Rahmen der Vorlesung zu weit
führen.
Wenn Sie in die Vorlesung gehen, später in der fortgeschrittenen
Objektorientierung, werden Sie das genauer erklärt bekommen, warum das
über den Rückgabetyp nicht geht.
Da gibt es eine ganze Theorie dahinter.
Für die, die sich ein bisschen belustigen wollen im Studium, Co- und
Kontravariante -Typsysteme ist das Stichwort.
Wenn Sie das googlen, finden Sie die Erklärung dazu.
Wichtig ist hier, auch Konstruktoren können überladen werden.
Das hatten wir tatsächlich auch schon mal gehabt, dass sich
Konstruktoren nur über Parameter unterscheiden.
Parametertypen.
Und das ist sogar sehr häufig.
Das ist ein ganz gängiger Programmierstil, dass man viele
Konstruktoren angibt, weil man einfach sagt, es gibt unter Umständen
verschiedene Kontexte, in denen man eine Instanz erzeugen will.
Manchmal sind die Parameter da, manchmal sind die Parameter da.
Und dann hat man viele Konstruktoren für dieselbe Klasse.
Und da ja jeder Konstruktor immer den Klassennamen haben muss, ist
klar, der muss überladen werden.
Das geht gar nicht.
Sie haben gar keine Freiheit, gerade bei der Namenswahl.
Dann gibt es beim Thema Methoden, jetzt wird es wieder ein bisschen
leichter, auch tatsächlich Methoden, die Sie definieren können, als
Hilfsmethoden.
Und zwar, wenn man jetzt in unserem Beispiel die Länge einer Linie
berechnen will, das braucht man vielleicht in verschiedenen Kontexten,
dann, wenn man das mit der architischen Geometrie macht, kann man das
so machen, indem man halt die Länge einer Linie berechnet.
Die Wurzel aus der Summe der Quadrate berechnet.
Und dann könnte man sich in Java eben, also die Formel quasi in Java
umgesetzt, die folgende Methode vorstellen, dass man sagt, ich möchte
im Prinzip eine Routine Length haben in meiner Klasse, die mir die
Länge berechnet.
Zwischen Start und Ende.
Und dann wende ich eben diese Formel an und gebe das Ergebnis zurück.
Math.square, square root by the way, ist ein schönes Beispiel wieder
für eine statische
Methode.
Und zwar von der Klasse Math.
Auch hier zwingt sie wieder keiner, ein Objekt, eine Instanz von der
Klasse Math zu erzeugen, nur um Quadratwurzel aufzurufen, weil
Quadratwurzel ist ein perfektes Beispiel für eine Klassenmethode.
Es braucht überhaupt keine Attributwerte, es rechnet alleine.
Genauso wie unsere Fahrenheit-Konversion, einfach nur auf den
Parameterwert rum.
So.
Und jetzt kann man sagen, naja, dieses Quadrieren hier will ich jetzt
nicht, das macht den Ausdruck ein bisschen länger.
Vielleicht auch schlecht erwartbarer, weil irgendeiner vergisst dann
vielleicht mal, dass hier immer dieselben Werte stehen müssen und
diese Abhängigkeit kann auch mal verloren gehen.
Also will ich das etwas eleganter ausdrücken und ich möchte jetzt hier
einfach hinschreiben, es ist quasi, wie ich ja eben auch mündlich
gesagt habe, die Wurzel aus der Summe der Quadrate.
Und dafür definiere ich meine Hilfsmethode Square.
Und das kann man machen, die gilt dann erstmal nur innerhalb der
Klasse.
Ob sie von außen aufrufbar ist oder nicht, kann man steuern, das
werden wir nachher noch kennenlernen.
Aber wie gesagt, das geht.
Dass sie einfach sagen, Berechnungen, die ich halt an verschiedene
Stellen aufrufen will, die ich nicht jedes Mal neu hinschreiben will,
dafür definiere ich mir eine Methode.
Das wäre eben jetzt hier das Quadrieren.
So, dann gibt es noch eine ganz besondere Methode, das ist das Main.
Und Sie erinnern sich, das hatten wir in der allerersten Vorlesung
auch schon kennengelernt.
Da hatte ich Ihnen gesagt, das ist der Einsprungspunkt.
Wenn also der Java-Compiler übersetzt ihr Programm in den Bytecode.
Der Bytecode wird dann in den Java-Interpreter geladen, indem Sie eben
Java und dann als Kommandozeilenparameter Ihren Klassennamen
schreiben.
Und was dann der sucht, ist die Main-Methode.
Und dann wird genau da auch die Ausführung gestartet durch den Aufruf
der Main-Methode.
Und wenn Sie jetzt mehrere Klassen haben, die zusammenarbeiten sollen,
jede eine Main-Methode hat, dann klappt das auch nicht, weil das muss
eindeutig sein.
Sie können im Prinzip zwar durchaus in Ihrem Programm viele, viele,
viele Klassen haben.
Das ist auch durchaus üblich.
Selbst kleine objektorientierte Projekte haben mal schnell 50 oder 70
Klassen.
Aber Sie dürfen eine Main-Methode haben, weil sonst weiß man nachher
nicht beim Starten, welche Main-Methode ist denn gewollt.
Die ist...
public, das heißt, sie ist von außen aufrufbar.
Sie ist static, das heißt, es gibt sie nur einmal, nicht je nach
Objektdistanz noch mal neu.
Braucht man nicht.
Und jetzt kommt das Besondere, sie hat ein Array von Strings.
Das Array, das kommt in der nächsten Vorlesung dran, was das ist.
Sie müssen sich vorstellen, das sind viele Strings hintereinander, die
man dann über 0, 1 usw.
ansprechen kann.
Das kommt in der nächsten Vorlesung dran.
Hier nur an der Stelle, das sind die Kommandozeilen-Parameter.
Wenn Sie hinschreiben, z.B.
in dem Falle Java und dann Leerzeichen und dann Ihr Programm, dann ist
für den Java-Interpreter Ihr Programm ein Kommandozeilen-Parameter.
Wenn Sie weitere mit Leerzeichen abgetrennte Parameter angeben beim
Aufruf von der Kommandozeile, werden die dann hier zugreifbar in
diesem Args Array.
Mit 0 geht es los und dann Args 1 usw.
Sehen Sie jetzt hier das Beispiel, wenn Sie z.B.
ein kleines Addierprogramm haben, was zwei Zahlen addiert und Sie
wollen das von außen einlesen, also beim Kommandozeilen-Aufruf schon
mitnehmen und jetzt nicht über den Scanner im Prinzip einlesen,
sondern über die Tastatur nachträglich, dann rufen Sie auf Java
myAdder und dann jetzt hier als beide Kommandozeilen-Parameter 2,0 und
3,0, immer ein Leerzeichen als Trennung und Args 0, das Array wird von
0 angezählt, ist dann eben das hier als String, deswegen die
Anführungszeichen und Args 1 ist irgendwie 3,5 als String und dann
müssen Sie das eben konvertieren, das kommt auch in einer der späteren
Vorlesungen in eben Double-Werte z.B.
oder Float-Werte, um dann mit denen zu rechnen.
Erstmal sind das natürlich Strings, also Zeichenketten, aber das ist
ja klar, das ist ja das, was Sie über die Kommandozeile eingeben.
Nur, dass Sie das einmal gesehen haben, was es mit diesem Args auf
sich hat und wie gesagt, das Thema Arrays, also diese eckigen Klammer
kommt in der nächsten Vorlesung noch ausführlich dran.
So, dann haben wir schon ein paar Sachen gesehen für die Ein- und
Ausgabe, das ist im Wesentlichen in großen Teilen eigentlich eine
Zusammenfassung von dem, was Sie schon gesehen haben.
Das System.out.println für eine Zeichenkette, das hatten Sie schon
gesehen.
Dann das mit dem Scanner, um über die Tastatur Eingaben einzulesen,
haben Sie auch schon gesehen.
Nextline, gibt einen String zurück.
Nextinteger, Sie können auch einen String konvertieren in einen
Integer, das ist das, wenn Sie zum Beispiel von der Kommandozeile
jetzt einen String bekommen.
Kann man parseInt machen oder parseDouble.
Parse ist ein englisches Wort, to parse, das heißt so viel wie
eigentlich zerteilen, zerlegen.
Im Prinzip nehmen Sie damit, wenn man so will, die Zeichenkette und
konvertieren das dann halt, wenn es geht, in eine Zahl.
So, und jetzt sieht man hier auch den Code von unserer Klasse MyAdder,
die ich eben aufgerufen habe.
Sie sagen halt, ich habe hier meine Routine main und ich habe jetzt
hier drei Variablen x, y und z.
x ist der erste Kommandozeilenparameter, der wie gesagt mit 0
dereferenziert wird hier.
y ist der zweite Kommandozeilenparameter, also args1 und das Ergebnis
ist dann z ist gleich x plus y, z hier als lokale Variable auch.
Und dann gebe ich das z aus.
Das wird automatisch jetzt wieder in einen String konvertiert, Sie
erinnern sich ja, System.out.println nimmt einen String, das geht aber
automatisch hier, dass man das z auch hier angibt als Double, das wird
aber automatisch konvertiert in einen String.
Und jetzt sehen Sie das Beispiel hier, also ich rufe java.myadder.2.0
.3.0 Das Ergebnis ist dann logischerweise 5,5.
Und hier sehen Sie eben dieses Spiel mit den Kommandozeilenparameter
und dass man die halt konvertieren muss in den entsprechenden
Datentyp, mit dem man rechnen kann.
Strings können Sie nicht addieren, das Plus bei Strings heißt einfach
nur aneinander.
So, auch jetzt das nächste ist auch nicht so rasend schwer.
Wenn man sich das Ganze jetzt mal zusammensetzt, was wir schon hatten,
wir haben zum Beispiel das Billard-Spiel, da haben wir die Main
-Routine.
Die Main-Routine erzeugt den Billard-Tisch, den Table und die Bälle
und fügt dann einen Ball an den Tisch hinzu.
Und das Ganze sieht dann so aus, ich habe drei Klassen, einmal die
Klasse Billard, einmal die Klasse Table und einmal die Klasse Ball.
Und Sie sehen, aber Main gibt es nur einmal.
Und Table hat den Konstruktor und die Routine AddBall und von mir ist
auch StartSimulation, was heißt also, wir starten jetzt quasi das
simulierte Billard-Spiel.
Und der Ball ist im Prinzip eine reine Entität, die nur sagt, wo ist
er, welche Farbe hat er und auf welchem Tisch liegt er.
Den Tisch müsste man nicht zwanghaft mitmodellieren bei Ball, weil der
Tisch weiß ja, welche Bälle drauf sind, der Ball muss nicht unbedingt
wissen, auf welchem Tisch er liegt.
Ist aber manchmal ganz bequem, wenn man auch die Abhängigkeit in
beiden Richtungen hat.
So, und dann gibt es hier den Konstruktor und hier dann eben z.B.
die Position, wo man einen Ball an eine bestimmte Stelle setzen kann.
Und auf die Art und Weise sehen Sie, kommen auf einmal drei Klassen
zusammen, aber immer wie gesagt nur eine Main-Methode, die dann
zentral steuert, was passiert.
So, und jetzt machen wir ein kleines Quiz zur Auflockerung und zwar
bekommen Sie als Aufgabe diesen Code, ja, hier tauchen auch Fu und
Bass auf und Bar und jetzt überlegen Sie sich erstmal gemeinsam oder
mit Nachbarn oder alleine, wozu gehören diese Methoden hin, zu welchen
Klassen und welche Signatur haben Sie.
Und wenn dieses Kästchen davor ist, dann ist der Rückgabetyp gefragt
von Fu, hier ist euch der Rückgabetyp von Bar gefragt und der
Parametertyp von Bar.
Und im Prinzip können Sie sich das alles erschließen aus diesem
Programmtext.
Sie müssen halt nur ein bisschen aufmerksam hinschauen quasi, was hier
wie aufgerufen wird.
Und das gehen wir dann auch gleich gemeinsam durch.
So, schauen wir uns das mal gemeinsam an.
So, also schauen wir uns erstmal die Routine Fu an Hier ist ja die
Frage, zu welcher Klasse gehört die.
Da gibt es einen Hinweis und zwar U.fu wird hier aufgerufen.
U ist eine Instanz der Klasse Unknown, habe ich ja hier deklariert.
Also gehört offenbar Fu, weil es über ein Objekt der Klasse Unknown
aufgerufen wird, wohl zur Klasse Unknown.
Ja, hier wird PowerPoint zum Feinsten aufgefahren.
So, jetzt ist die spannende Frage, was ist denn jetzt der Rückgabetyp
von Fu?
Das ist ja hier an der Stelle gefragt.
Jetzt kann man sich das mal ein bisschen anschauen.
Wo wird denn jetzt hier alles was zurückgegeben von Fu?
Hier wird ein Fu offenbar zurückgegeben und darauf kann man dann wohl
einen BOM aufrufen, wie man hier an der Stelle sieht.
Auf dem was hier Fu zurückgegeben hat, kann man nochmal Fu aufrufen,
sieht man hier.
Wir haben eben herausgefunden, Fu gehört zu Unknown.
Also auch wieder Unknown.
Sonst hätte man einen Typfehler.
So, jetzt geht es weiter mit Bar, also nicht Bass, sondern Bar.
Wo sehen wir das Bar?
Das Bar wird hier auf ein Ergebnis von Fu aufgerufen.
Also ist klar, gehört das eben auch zu Unknown, weil sonst wäre Bar
nicht aufrufbar, weil es eben auf dem Ergebnis von Fu aufrufbar ist.
Und das wissen wir ja von oben, das ist ein Unknown.
So, was ist jetzt die Signatur?
Was gibt offenbar das Bar zurück?
Na ja, Bar gibt zurück.
Wo sehen wir das?
Bei dieser Zuweisung B.
Also gibt es einen Boolean zurück.
Und was ist der Parameter?
Na ja, das sehen wir hier ziemlich direkt.
Das ist ein U.
U ist ein Unknown, also wieder Unknown.
So, geht das Spielchen weiter.
Jetzt schauen wir uns Bass an.
Das hier.
Bass, sehen Sie, wird nicht aufgerufen mit einem Objekt.
Wenn Sie jetzt schon denken, oh super, das habe ich eben gelernt, das
ist eine Klassenvariable, äh Klassenmethode.
Nein, ist es nicht.
Steht nämlich auch kein Klassenname da.
Sondern es steht gar nichts hier.
Wenn gar nichts da steht, dann ist es der Trivialfall, es ist einfach
eine Methode dieser Klasse.
Schlusspunkt aus, ja.
Auch wenn Sie jetzt hier nicht sehen, wo sie definiert ist, aber
offenbar gehört sie zu Quiz 2.
Weil sonst hätten Sie irgendein Objekt oder einen Klassennamen davor
schreiben müssen.
Und es hat die Signatur.
Na ja, was die Rückgabe ist, sehen wir eigentlich hier relativ
schnell.
Das ist ein Unknown.
Und es ist quasi ein Ergebnis von Bar.
Und Bar, können wir ja nachgucken, hat als Ergebnis Typ Boolean.
Also müssen wir das jetzt auch hier so finden.
Tatsächlich.
Unknown gibt es zurück.
Das ist hier die Zuweisung und Boolean ist das, was von Bar ja als
Parameter reinkommt.
So, jetzt haben wir nur noch eine Klasse übrig, das ist das Bomb.
Und Bomb, können wir uns überlegen, wozu gehört es?
Na ja, offenbar zu dem, was foo zurückgibt.
Was foo zurückgibt, kennen wir ja schon wirklich längst.
Das ist Unknown.
Und was ist das Ergebnis von Bomb?
Na ja, jetzt müssen wir ein bisschen zurückgucken.
Gleichheitsweise ein x.
x ist ein Integer.
Also ist das ein Int.
Also, Sie sehen eigentlich rein logisch, aber man muss halt schon ein
bisschen nachdenken.
Es soll so ein bisschen auch eine Programmleseübung für Sie gewesen
sein.
Rasend guter Stil ist das ehrlich gesagt nicht mit diesen langen,
verketteten Aufrufen.
Also man kann das natürlich machen, der Compiler schluckt das auch,
aber auch hier wieder, das werden Sie in späteren Vorlesungen, z.B.
bei mir oder bei Frau Koziolek in der Softwaretechnik 2 Vorlesung
hören, da reden wir dann vielmehr auch über Stilfragen und da gibt es
eine Regel, das heißt Law of Demeter, dass man so Aufrufketten nicht
will und da gibt es auch Regeln, wann darf man Aufrufketten machen und
wann nicht.
Aber jetzt erstmal für Sie hier, das ist einfach nur eine
Programmleseregel, eine Leseaufgabe gewesen.
So fassen wir das Ganze zusammen, was wir bis jetzt hatten, aber auch
in der Vorlesung vorher.
Wir haben einmal über Konstruktoren gesprochen und die Konstruktoren
haben den Charme gehabt, dass sie zum einen die Erzeugung des
Objektes, also das Besorgen des Speicherplatzes, das Setzen der
Objektreferenz, aber eben auch die Initialisierung der Attribute mit
übernehmen.
Und das Ganze wird so deklariert, dass man sagt, ich gebe den
Klassennamen an.
Damit ist klar, diese Methode ist keine normale Methode, sondern der
Konstruktor.
Und kann eben in Verbindung mit new aufgerufen werden von außen.
Und dann kommen die formalen Parameter und der Methodenrumpf ganz
genau normal, wie bei jeder Methode auch.
Insofern sind Konstruktoren Spezialfälle von Methoden, aber es sind
eben die Methoden, die eben als Namen den Klassennamen haben.
Den kann man halt anders nicht verwenden.
Und wir hatten auch gelernt, dass wir dieses Wort this nehmen können,
um sicherzustellen, dass man jetzt auch auf ein Attribut zugreift.
This.x war das Attribut x der Klasse und nicht der formale Parameter
x.
So, dann haben wir das Ganze verallgemeinert.
Wir haben über Methoden gesprochen.
Methoden nimmt man einfach, um das Verhalten vom Objekt zu
beschreiben.
Sie erinnern sich, das Verschieben der Billardkugel, das Rotieren der
Billardkugel.
Aber einfach auch, um Berechnungen oder überhaupt Ausführungssequenzen
zu kapseln.
Das war zum Beispiel unsere Hilfsmethode Square gewesen.
Wir hatten gelernt, standardmäßig ist erstmal alles eine
Objektmethode.
Aber es gibt auch die Ausnahme von einer Klassenmethode.
Nämlich dann, wenn Sie eine Methode nutzbar machen wollen, ohne dass
man ein Objekt erzeugt.
Das geht genau nur dann, wenn Sie auch keine Objektattribute nutzen.
Wenn Sie eine Methodenrumpf haben, der tatsächlich immer nur ohne
Attribute arbeitet, dann braucht man nicht zwanghaft ein Objekt dafür.
Dann wäre es auch umständlich, vielleicht immer den Nutzer zu zwingen,
den nutzenden Entwickler zu zwingen, ein Objekt zu erzeugen.
Und dann nimmt man, bitte nur dann, eine Klassenmethode.
Wie gesagt, bevor Sie sich jetzt denken, wie kann ich die Anteil der
Klassenmethoden maximieren?
Nein, das ist nicht die Aufgabe.
Das ist der Ausnahmefall.
Das ist deutlich seltener.
Dann haben wir über lokale Variablen gesprochen, die halt nur gelten
im Gültigkeitsbereich oder während eine Methode aufgerufen ist.
Wenn Sie die Methode mehrfach verschachteln, aufrufen, wird auch jedes
Mal neu die lokale Variable erzeugt.
Eine Sache, die ich vergessen habe zu sagen, die lokalen Variablen
werden witzigerweise nicht initialisiert.
Sonst wird in Java jedes Attribut immer defaultmäßig initialisiert.
Lokale Variablen nicht.
Hintergrund kann man lange diskutieren.
Waren wohl Überlegungen, so Performancegründe und so weiter, aber
vielleicht ein bisschen inkonsistent.
So, und dann hatten wir das Return-Bot noch kennengelernt.
Damit beendet man eine Methode.
Wir haben das klassischerweise immer gesehen am Ende einer Methode.
Der Parameter von Return kann entweder gar keiner sein.
Das heißt einfach nur bei einer Void-Methode, wenn ich keinen
Rückgabetyp habe, kein Ergebnis habe, ich beende einfach die
Ausführung an der Stelle, springe zurück zum Aufrufer.
Wenn ich einen Ausdruck mitgebe, dann wird der evaluiert und ist dann
das Ergebnis der Methode.
Und, auch wenn wir das jetzt nicht gesehen haben, Sie können durchaus
an verschiedenen Stellen auch Returns haben.
Wenn Sie nämlich vorher zum Beispiel den Kontrollschluss verzweigen
mit Ifs oder so, dann können Sie auch verschiedene Returns haben.
Ansonsten macht es natürlich keinen Sinn bei einem rein sequentiellen
Ablauf verschiedene Returns zu machen, weil das Erste beendet die
Ausführung.
Alle anderen dahinter werden eh nicht mehr genutzt.
Kurz noch der Literaturhinweis.
Kapitel 6 und 8 können Sie auch nutzen, hier von dem Buch von Raths.
Übrigens habe ich jetzt erfahren, es gibt noch mittlerweile eine 8.
Auflage, ganz frisch rausgekommen.
Das hier steht noch die 7.
Aber wie gesagt, mittlerweile gibt es auch die 8.
Und wie gesagt, da können Sie das nachlesen.
Die Angaben stimmen nach wie vor.
Und für diejenigen, die sich für das ganze Thema so Typsysteme und
wann kann man wie diskriminieren beim Überladen.
Das ist hier ein deutlich anspruchsvolleres Buch.
Es ist kein Anfängerbuch zum Programmieren lernen, sondern es erklärt
mehr die Hintergründe, wie ein Typsystem funktioniert.
Das ist quasi so jetzt für diese Einstellfolien.
Hörer, Hörerinnen.
Das heißt Types and Programming Languages von Benjamin Peirce.
Absoluter Klassiker.
Benjamin Peirce auch super berühmt.
Und wie gesagt, da erfährt man mehr über die Hintergründe und versteht
auch dann eben besser, was ich jetzt hier nicht ganz zu Ende erklärt
hatte.
Warum kann man beim Überladen nicht über den Rückgabetyp
diskriminieren?
Warum geht das immer nur über die Parametertypen?
Aber wie gesagt, natürlich keine Pflichtliteratur, aber für die, die
jetzt, sag ich mal, so ein bisschen mehr die Hintergründe kennen
wollen, ist das ein spannendes Buch vielleicht.
Dann nochmal gerade eben das große Bild.
Wir haben uns jetzt hier ein bisschen um die eine Ausgabe gekümmert,
um Methoden.
Wir haben hier das Verhalten kennengelernt jetzt schon.
Und kommen jetzt tatsächlich bei der nächsten Vorlesung mal wieder zu
Datentypen.
Genau, das ist was wir jetzt schon alles bisher gemacht haben.
Und jetzt, wie gesagt, geht es wieder weiter mit zusammengesetzten
Datentypen und Arrays.
So, in dieser Vorlesung geht es um Arrays.
Wir sind also an dieser Stelle jetzt hier.
Arrays ist eine bestimmte Art, wie man aus einfachen Datentypen
sogenannte komplexe Datentypen machen kann.
Eine Art und Weise haben Sie schon längst kennengelernt, wenn man so
will, Klassen bauen.
Klassen nehmen Sie auch verschiedene Datentypen, bauen einen neuen
Datentyp drumherum, der ist in dem Sinne komplex, dass er
zusammengesetzt ist aus anderen Datentypen.
Jetzt war es beim Objekt oder bei der Klasse so gewesen, das haben Sie
schon gesehen, die Attribute können einen anderen Typ haben.
Die müssen nicht alle denselben Typ haben.
Bei Arrays ist das anders.
Da haben Sie jetzt einfach mal viele Variablen, die aber alle
denselben Typ haben und alle über denselben Bezeichner ansprechbar
sind.
Das gucken wir uns jetzt mal im Detail an.
Gerade beim Überblick, Sie merken so ein bisschen, wir schleppen in
der Vorlesung.
Ich erzähle zu viel Einstein-Gedönse.
Eigentlich hätte das schon letzte Woche, also müssten wir heute das
hier alles hinbekommen.
Kriegen wir wahrscheinlich aber nicht hin, wir werden wahrscheinlich
noch nicht mal mit den Arrays durchkommen.
Ist aber kein Problem, wir haben das mit den Übungsblättern abgepasst,
also Sie brauchen jetzt nicht jammern.
Alles gut.
Und wenn ich Ihnen zu viel Einstein-Gramm erzähle, können Sie das auch
sagen.
Sie können auch sagen, zu wenig, aber dann schleppen wir noch mehr.
Arrays, wie sagt diese auf Deutsch, kann man sagen, wenn man wie
deklariert man die, wie initialisiert man die, wie kann man darauf
zugreifen und wie kann man das Ganze in einem Programm auch einsetzen.
Und werden wir vielleicht heute nicht mehr schaffen, je nach Zeit,
dann werden wir auch noch ein neues Schleifenkonstrukt, die For-Each
-Schleife kennenlernen und wir werden auch noch als kleine geistige
Übung mehrdimensionale Arrays kennenlernen.
So, also Array, so Deutsch Feld, ist erstmal eine Folge von mehreren
Variablen, die aber alle denselben Typ haben.
Das ist eben anders wie bei der Klasse.
Bei der Klasse kann ich ganz verschiedene Attributtypen nehmen und
zusammen sagen, die sind jetzt die Attribute meiner Klasse.
Das geht aber beim Array eben gerade nicht.
Das Array hat aber einen großen Vorteil.
Sie können tatsächlich alle über den Namen des Arrays angesprochen
werden, um aber jetzt zu sagen, welches Element nehme ich, gibt es
einen sogenannten Index.
Das ist ein Integer.
Und hier sehen wir mal ein Beispiel.
Ich habe jetzt hier ein Array von Double-Zahlen und da habe ich die 0
,7, die 23,2 0,003 und so weiter.
Die haben alle überhaupt keinen Sinn, diese Zahlen.
Das sollen einfach nur beliebige Gleitkomma-Zahlen sein.
Und jetzt sieht man aber, die hier wird über den Array-Namen 0, in
eckigen Klammern mit 0 angesprochen, die 1 und so weiter.
Und was man sieht, ist also, das Ganze wird immer bei 0.
Schau mal, fängt das an.
Kann man gut finden oder nicht.
Ist aber so, das kommt aus der C-Welt.
Oder aber, das kann das Ganze auch, jetzt muss ich mal Gleitkomma
-Zahlen, das können auch Strings sein.
Wir können im Prinzip von jedem beliebigen Klasse oder von jedem
beliebigen Elementar-Daten-Typ Arrays bauen.
Hier jetzt mal von Strings.
Aber wieder dasselbe, es wird mit 0 quasi angefangen zu referenzieren.
Das Ganze sieht dann so aus, wenn man das Ganze deklariert, schreibt
man den, kennen Sie das bisher schon, Sie schreiben Typ und Name, aber
diesmal schreiben Sie noch diese eckigen Klammer dahinter.
Damit haben Sie aber erstmal nichts anderes gemacht, als eine Referenz
erzeugt auf das Array.
Das neue Array müssen Sie dann tatsächlich erzeugen, entweder zur
Laufzeit mit new, also eigentlich wie ein Konstruktor-Aufruf, den Sie
machen, und dann den Typ hinter new dahinter, also den Klassen-Namen,
und dann aber den eckigen Klammern mit n, wie viele wollen Sie haben.
Die eckigen Klammern haben also zwei Bedeutungen.
Wenn man ganz pingelig ist, sogar drei.
Einmal heißen die eckigen Klammern wie hier, Guten Tag, lieber
Compiler, das hier soll ein Array werden und nicht nur einfach so ein
normaler Typ, sondern ein Array von diesem Typ.
Zweite Bedeutung von eckigen Klammern, erzeugt mir mal bitte n
Instanzen hiervon.
Dritte Bedeutung werden wir gleich beim Nutzen von Arrays sehen.
Das heißt, in den eckigen Klammern gibt man den Index an und sagt
genau, welches Element von diesem Feld will ich haben.
Aber jetzt sind wir erstmal noch bei der Deklaration.
Es gibt noch die Möglichkeit, wenn es gar nicht so viele Elemente
sind, und Sie es schon alle kennen, dass Sie das dann hier so mit
geschweiften Klammern einfach die Elemente angeben und damit das Array
quasi auch die Objektidentität erzeugen und auch gleich dann schon
initialisiert haben.
Aber sehen wir jetzt hier als Beispiel, Sie haben ein Array von
String, das heißt S und das wird initialisiert mit den Werten Meier,
Meier, Meier, Meier.
Das reserviert jetzt für vier Strings den Speicherplatz und
initialisiert auch diese vier, also S-eckige Klammer 0 ist Meier, S
-eckige Klammer 1 ist Meier, S-eckige Klammer 4 gibt es nicht.
Hier habe ich nur bis 3.
Man zählt hier 0, 1, 2, 3.
Hier sieht man ein anderes Beispiel.
Hier haben Sie ein Byte Array und Sie erzeugen das und sagen hier für
12 Bytes möchte ich Speicherplatz haben.
Die Indizes sind wieder 0 bis 11.
Oder hier sagen Sie ich möchte eine Referenz haben auf ein Array A,
ein Integer Array A, aber es gibt noch keinen Speicherplatz dafür.
Das müssen Sie mal später, genau wie das mit den Objekten ist, wenn
Sie eine Objektreferenz haben, müssen Sie das später mal irgendwann
bevor Sie damit arbeiten, einen U-Aufruf machen und das Ergebnis der
Objektreferenz zuweisen und genauso ist das hier mit dem Array letzten
Endes auch.
Wenn man sich das versucht nur ein bisschen bildlich vorzustellen,
wenn ich jetzt sage ich habe ein Array von Gleitkommazahlen mit
doppelten Genauigkeit, also Double Array A ist gleich New Double 8,
dann haben Sie hier 8 mal von 0 bis 7, den Wert 0, weil standardmäßig
wird ein Double mit 0,0 initialisiert im Java.
So dasselbe können Sie auch mit Strings machen.
Sie können natürlich auch ein String Array machen.
Diese Strings werden jetzt aber auch alle mit Null initialisiert.
Das ist also nicht der Wert 0, sondern das ist, Sie erinnern sich,
String ist eine Klasse, da wird ja auch groß geschrieben.
Das heißt, was Sie hier sich erzeugt haben, sind 100 Objektreferenzen
auf Strings.
Wir haben nicht 100 Identitäten erzeugt.
Jetzt werden Sie sagen, wie denn das?
Ich habe doch hier New aufgerufen.
Ja, aber das ist das New für das Array.
Sie haben jetzt quasi diesen Speicher für diese ganzen
Objektreferenzen sich mit dem New besorgt, nämlich für 100
Objektreferenzen vom Typ String.
Ich habe aber nicht 100 Objektidentitäten gemacht.
Dafür müssen Sie nämlich irgendwie 100 mal New aufrufen.
Clevererweise natürlich in der Schleife, nicht durch Hinschreiben von
New.
Hier, das ist für die andere Initialisierungsartenweise das Beispiel,
sagen Sie, ich möchte ein Integer-Array haben, das soll P heißen.
Und ich gebe es auch tatsächlich gleich an, welche Integers
drinstehen.
Damit ist auch die Länge klar.
Nämlich 5.
5 Elemente hier.
Und das sieht im Speicher dann so aus, dass P quasi die Adresse hat
von diesem Block hier, wo genau diese Werte auch drinstehen als
Integer -Array, also 2, 3, 5, 7, 11.
Wo Sie dann auch über P, eckige Klammer 0 bis eckige Klammer 0,4
darauf zugreifen können.
Und hier sieht man jetzt tatsächlich, wie man das Ganze ausfließt.
Das ist, wenn Sie so wollen, jetzt die nächste Semantik, die dritte
Semantik von dieser eckigen Klammer.
Das heißt einfach, nimmt das Element in der eckigen Klammer, diesen
Integer hier und weise dem, jetzt A.0, also dem ersten Element die 3,5
zu, dem vierten Element die 7,9 und dem siebten Element die minus 22
,3.
Und ich kann es jetzt auch genauso auslesen.
Im Prinzip wieder hier, eckige Klammer A, eckige Klammer 3, das vierte
Element hier.
Und das kann ich natürlich jetzt hier durch ein Zuweisen oder Ausgeben
direkt oder so.
Dann gibt es eine Variable, auf die können Sie nur lesend zugreifen.
Das ist eine sogenannte Final-Variable.
Das ist die Länge des Arrays.
Es kann ja sein, dass Sie an irgendwelchen Stellen im Programmcode das
Array vor sich haben und Sie nicht wissen, wie lange ist das Array.
Das ist nicht nur, weil Sie zu faul sind, um nachzugucken, wie es
deklariert wurde.
Sondern Sie wissen ja mit new wenn Sie jetzt hier sowas aufrufen, da
wo jetzt hier die feste 8 oder die feste 100 steht, könnten Sie eine
Variable einsetzen.
Das heißt, Sie wissen wirklich nicht im Vorfeld, wie viele Instanzen
Sie haben.
Und wenn Sie das dann mal später wissen wollen, können Sie mal mit a
.length auf die Länge zugreifen.
Und hier sieht man mal ein Beispiel.
Sie erzeugen sich jetzt ein Array von den ersten 5 Primzahlen und dann
können Sie mit L zum Beispiel die Länge auslesen.
Das ist natürlich jetzt hier relativ witzlos, weil Sie wissen schon,
dass es 5 sind, aber wie gesagt, wenn Sie jetzt mit new, eckige
Klammern das Ding erzeugt hätten, wüssten Sie vielleicht nicht, wie
viele Elemente das sind, weil da nicht immer eine Konstante drin sein
muss.
Und also wie gesagt, und das ganze length ist eine Klassenvariable von
Array und die ist als final int deklariert, das heißt Entschuldigung,
keine Klassenvariable, das ist schon eine Objektvariable, aber sie ist
als final int deklariert, das heißt, sie ist eine Konstante.
Sie können sie nur lesen, können aber nicht darauf zuweisen, was auch
keinen Sinn macht.
Also Sie können nicht die Länge von einem Array vergrößern, indem Sie
da einen größeren Wert reinschreiben.
Das geht nicht.
Sie können es immer nur auslesen.
So, jetzt können wir mal schauen, wie man damit umgeht mit diesen
Arrays.
Wir erzeugen uns ein neues Double Array von der Größe N.
N jetzt mal hier in der Variable drinnen, die man auf den Wert 100
vorher gesetzt hat.
Und jetzt füllen Sie das mit Zufallswerten und das machen Sie
clevererweise in einer Schleife und iterieren quasi hier als Index,
haben Sie jetzt hier die Laufvariable I und damit laufen Sie im
Prinzip durch.
Das I wird ja bei jedem Schleifen durch Lauf erhöht, Sie erinnern
sich, wie das war mit diesen Vorschleifen.
Und im Prinzip wird jetzt alle von A bis 0 bis N-1 angesprochen und
jetzt mit einem Zufallswert erhöht.
Und das ist auch ein ganz klassisches Idiom, was Sie jetzt sehen, dass
Sie in dem Fall jetzt ein echt kleiner Zeichen hier haben.
Das liegt daran, weil Sie mit 0 ja anfangen müssen.
Das ist so in Java.
Das erste Array-Element ist mal mit 0 indiziert.
Damit ist aber klar, auch wenn Sie Groß-N-Filelement haben, dürfen Sie
natürlich nie A in eckigen Klammern Groß-N ansprechen.
Das gibt es nicht mehr.
Sie haben nur A Groß-N-1 als letztes Element.
Und genau das wird auch sichergestellt, dass hier ein echt kleiner
steht.
Und Sie können sich einfach mal merken, wenn Sie Programmtext lesen,
wenn da ein kleiner Klei steht, ist es meistens ein Fehler.
Oder wirklich einer der wenigen Ausnahmefälle, wo man auf andere Art
und Weise sicherstellt, dass das hier nicht schief geht.
Indem man z.B.
hier immer A-1 reinschreibt und mit 1 die Schleife loslaufen lässt.
Aber das ist natürlich nicht so schön, weil man jedes Mal eins
abziehen muss.
Das ist ja ziemlich bescheuert.
So, dann kann man z.B.
sagen, ich möchte eine Array wirklich mal echt kopieren.
Und zwar besorgt man sich jetzt ein zweites Array.
D.
Das hat die Länge von A.
Und jetzt iteriert man dadurch mit der Länge von A, A Punkt längs.
Sie sehen, ich lese das hier aus, diesen Wert.
Und mache jetzt hier diese Zuweisung und kopiere damit alles in die
entsprechenden Werte von B rein.
Ich weiß, das kann man kürzer mit einer Weilschleife programmieren,
aber darauf kommt es jetzt nicht an.
Dann kann man sagen, ich möchte z.B.
mal den Mittelwert berechnen von allen Werten in einem Array.
Und zwar sage ich am Anfang die Summe 0, dann summiere ich auf.
Sie erinnern sich, dieses Plusgleich heißt nichts anderes wie Sum ist
gleich Sum plus A i und da Sie das i wieder durchlaufen von 0 bis 1
weniger als das n.
Haben Sie jetzt alle Werte aufsummiert und dann müssen Sie das durch
das n teilen, um halt sicherzustellen, was Sie im Durchschnitt
gebildet haben.
Jetzt werden Sie zu Recht sagen, was ist denn, wenn n Null ist.
Für die Schleife ist das kein Problem, die wird dann nicht ausgeführt.
Aber dann steht hier natürlich Null durch Null und das sollte nicht
hier stehen.
Dann kann man sagen, ich möchte z.B.
das Array spiegeln.
Ich möchte im Prinzip die Reihenfolge umdrehen.
Das ist ein bisschen aufwendiger und das habe ich Ihnen jetzt nur
deswegen mal gezeigt, um Ihnen zu zeigen, Sie können auch Arithmetik
hier machen.
Also das müssen einfach nur Integer-Ausdrücke sein.
Das muss keine Konstante oder Variable sein, das kann auch ein
Ausdruck sein.
Und Sie sehen jetzt tatsächlich, ich durchlaufe nur die Hälfte von dem
Array und im Prinzip die von 0 bis n halbe und im Prinzip vertausche
ich jetzt immer die Werte im Prinzip mit dem entsprechenden Index.
Und das mache ich halt im Prinzip dieses Vertauschen, indem ich noch
eine temporäre dritte Variable, die ich hier im Spiel habe, also ich
habe die beiden, wo ich rauslese, wo ich hinschreibe und ich brauche
noch eine temporäre Variable zum Tauschen, weil sonst habe ich ja
schon einmal überschrieben und habe den Wert nicht mehr.
Und deswegen speichere ich mir den Wert von i, also in der ersten
Arrayhälfte den Wert, überschreibe den dann mit dem Wert
spiegelbildlich von hinten gespiegelt und nehme dann das Tempwert, um
quasi die zweite Hälfte vom Array hinten zu beschreiben wieder.
Und das iteriere ich halt durch, aber dann logischerweise auch nur bis
zur Hälfte hier.
So, das sind einfach Beispiele, wie man Arrays verwenden kann und Sie
sehen schon, diese Arrays haben vor allem ihre Mächtigkeit daran, wenn
Sie die in Schleifen verwenden.
Weil Sie im Prinzip, stellen Sie sich vor, Sie hätten alle diese
Elemente nicht in einem Array, sondern mit lauter einzelnen Variablen
mit unterschiedlichen Namen benannt, mal abgesehen davon, dass das
wahnsinnig viel Aufwand bei der Deklaration ist.
Es ist auch dann super unflexibel, weil die Zahl ist fixiert, Sie
können ihn nicht mehr variabel halten.
Und mit einem Array können Sie halt im Prinzip noch zur Laufzeit
entscheiden, wie groß wird es.
Und Sie können es auch nicht in der Schleife so einfach durchlaufen,
weil Sie nicht einheitlich diese Elemente adressieren können.
Und das ist gerade eben die Mächtigkeit von Arrays.
Wenn Sie eben häufig denselben Datentyp haben und Sie denken, das kann
mal notwendig sein, den in der Schleife zu bearbeiten oder es kann mal
notwendig sein, dass die Anzahl variiert, dann ist eben Array immer
besser, als wenn man das einzeln hinschreibt.
Mal abgesehen davon, dass bei großen Endes das eh absurd viel Arbeit
wäre.
Ah, so hier meinen Sie, genau.
Genau, das ist eine gute Frage.
Also, was Sie dann in dem Augenblick machen, ist, Sie haben zwei
verschiedene, das ist wirklich wie bei Klassen, Sie haben zwei
Objektreferenzen und Sie weisen die eine Objektreferenz dem anderen
zu.
Das heißt aber, Sie haben zwei Referenzen auf dieselbe Identität und
das heißt, wenn Sie das eine manipulieren, wird das andere automatisch
mitmanipuliert.
Sie sprechen es dann halt nur über A oder über B an.
Das ist im Prinzip genau wie bei Klassen, weil Sie im Prinzip nur eine
Objektidentität haben, zwei Objektreferenzen, dann klappt das nicht.
Also nochmal für alle, die Frage Ihres Kommilitonen war es, wenn ich
jetzt hier die Index weglasse, einfach nur sage, B ist gleich A,
könnte man ja sagen, das ist ja auch sowas wie ein Kopieren von einem
Array.
Es ist aber, wie gesagt, wie bei Klassen, Sie erinnern sich an unser
Vector -Beispiel, wo Sie nur zwei Referenzen haben auf dieselbe
Identität.
Das heißt, es ist nicht wirklich eine Kopie, sondern es ist wirklich
dasselbe.
Und wenn Sie das eine manipulieren, haben Sie dann auch das andere,
quasi das andere ist gar nicht ein anderes, auch manipuliert.
Das ist der Unterschied, weil hier machen Sie natürlich eine echte
Kopie.
So, jetzt machen wir ein kleines Quiz.
Und zwar haben wir drei mini kleine Codeschnipsel.
Und die Frage ist, ist das korrekter Code korrekt?
Im Sinne von syntaktisch korrekt oder auch korrekt im Sinne von, gibt
Probleme zur Laufzeit, auch wenn es vielleicht kompiliert hat.
Und jetzt schauen Sie sich das in Ruhe an, dann gehen wir das gleich
durch.
Welcher davon oder ist überhaupt einer davon korrekt?
So, schauen wir uns mal den ersten Code an hier.
Also, wer denkt, dass dieser Code korrekt ist?
Haha, traut sich keiner, ist gut.
Gegenfrage, wer enthält sich?
Weiß nicht.
Aha, also, inkorrekt, super, richtig.
Einfach mal reinrufen, warum ist er inkorrekt?
Bitte?
Teile zwei, ganz genau korrekt.
Hier ist eine Zuweisung auf A Punkt längs und das geht nicht.
Vollkommen richtig, genau.
Das hier, habe ich ja gesagt, längs ist eine finale Variable.
Finale heißt bei Variablen, man kann sie auslesen, aber man kann ihm
nicht mehr zuweisen.
Und das heißt, Sie können eben leider nicht quasi durch eine Zuweisung
auf längs die Länge des Arrays nochmal nachdrücklich manipulieren.
Das geht nicht.
Sie können es nur auslesen, aber nicht reinschreiben.
Deswegen, das würde auch der Compiler meckern, der würde sagen, hallo,
längs ist eine finale Variable, können Sie nicht zuweisen.
Das kompiliert nicht.
So, gucken wir uns das zweite an.
Ich gebe zu, das ist ein bisschen trickreicher.
Wer denkt, dass das zweite, also eine Array-Deklaration mit einem
LongInt, dass das korrekt ist?
Ein paar, okay.
Wer denkt, dass es inkorrekt ist?
Das sind ein paar mehr.
Es ist zugegebenermaßen inkorrekt, aber die Antwort ist ein bisschen
unbefriedigend.
Das ist einfach im Standard verboten.
Aber deswegen auch eine Information für Sie.
Sie können einen Index immer nur einen Integer machen und nicht eben
einen LongInt oder so.
Man kann sich ein bisschen überlegen, warum wohl.
Sie können hier ganz leicht, wie jetzt hier, Werte reinschreiben, die
den kompletten Speicher Ihres Rechners übertreffen, selbst wenn Sie
ein paar Gigabyte haben.
Und das ist halt das Problem.
Der Long-Bereich ist einfach zu groß mit den 64 Bit.
Und deswegen hat man dem Java-Sprachstandard einfach nur Ints erlaubt.
Das ist per Definition so.
Wie gesagt, es gibt auch durchaus einen Grund.
Aber es ist ein bisschen eine fiese Frage.
Aber ich soll Ihnen halt einfach nochmal das Wissen vermitteln.
Sie können hier immer nur Integer reinschreiben.
Schauen wir uns den dritten Code an.
Wer ist der Auffassung, dass der korrekt ist?
Ja, ein paar.
Wer denkt, dass er inkorrekt ist?
Ja, das ist die Mehrheit.
Einfach mal auch wieder reinrufen, wo ist denn der Fehler?
Inkorrekt stimmt nämlich.
Genau.
Hier an der Stelle haben Sie ein kleiner Gleich.
Und das kleiner Gleich ist tatsächlich falsch.
Sie greifen an der Stelle...
I kann also tatsächlich den Wert von Length haben.
Aber das ist ein Element zu viel.
Sie fangen ja bei Null an zu zählen.
Was Sie ja auch hier machen.
Und das heißt, Sie greifen hier...
Length hat ja den Wert 4.
Sie greifen hier auf C4 zu.
Und C4 gibt es gar nicht.
Und dieser Zugriff, den entdeckt nicht der Compiler für Sie.
Das Programm kompiliert.
Aber beim Ablaufen wird es dann auf einmal sein, dass Sie einen Wert
dereferenzieren wollen, der gar nicht dazugehört.
Und jetzt können Sie sagen, Moment mal, warum hat denn der Compiler
mich nicht gewarnt?
Und das ist genau das Problem, was wir schon mal in der letzten Folie
so ganz kurz angerissen haben.
Das ist das sogenannte Halteproblem.
Sie können nicht alle Eigenschaften Ihres Programms statisch prüfen,
ohne das Programm auszuführen.
Und der Compiler kann in der Tat nicht in jedem Fall im Vorfeld
wissen, welchen Wert hat jetzt Length, oder was steht jetzt hier
eigentlich drin.
In dem Fall könnten Sie sagen, naja, komm, hier steht eine 4, das ist
Mist, und Length ist eine Static Final Variante, das müsste der
Compiler doch entdecken.
Der Compiler wird Ihnen vielleicht, wenn Sie Glück haben, einen
Warning ausgeben, aber nicht mehr.
Das sind Dinge, die im Prinzip typmäßig korrekt sind.
Hier steht ein Integer, I, hier steht ein Integer, C Punkt Length für
den Compiler, bingo, alles super.
Nur zur Laufzeit knallt es natürlich.
Und deswegen, das haben Sie vielleicht auch schon beim Programmieren
leidvoll gemerkt, bloß weil der Compiler erfolgreich ihr Programm
kompiliert, heißt das natürlich nicht, dass es fehlerfrei ist.
Aber wie gesagt, das könnte auch statisch ein Compiler nie prüfen,
komplette Fehlerfreiheit.
Und deswegen, wenn man das aber jetzt hier korrigiert, wenn man das
hier in echt kleiner umwandelt und nicht kleiner gleich, dann ist es
auch korrekt, dann würde es funktionieren.
So, nehmen wir noch ein paar andere Beispiele, damit Sie das Hantieren
mit Schleifen auch ein bisschen nochmal sehen, weil das können Sie
auch fürs Übungsblatt brauchen und später auch für die Präsenzübungen
und für die Abschlussaufgaben.
Das ist einfach ganz wichtiges Handwerkszeug.
Hier möchte ich mal zählen in einem Array, was ich übergebe, beim
Integer Array A, wie häufig taucht denn die Nummer Number auf?
Und dafür habe ich eine Routine, Count Occurrences, also zähle
Vorkommnisse, Vorkommen.
Und am Anfang wird Count auf 0 initialisiert.
So, und dann hat man eine Variable i, das ist der Index, mit dem ich
durchläufe, auch korrekterweise natürlich echt kleiner A Punkt Length.
Und jetzt schaue ich nach, innerhalb der Schleife, innerhalb des
Schleifenrumpfes, entspricht diese Zahl A i der Nummer?
Und wenn ja, wenn das Ganze also true wird, dann erhöhe ich den
Zähler, Count++, Sie ja wissen,++ ist das Erhöhen um 1.
Und ansonsten mache ich nichts, erhöhe also auch den Zähler nicht und
gehe einfach in die nächste Schleife rein, durchlaufe das und am Ende
habe ich gezählt, wie häufig war das denn wahr, dass es ein i A i gab,
was gleichnamenbar war.
So.
Erstmal, ich hoffe, dass da alle jetzt irgendwo noch in Sync sind,
weil, wenn man drüber nachdenkt, klar, man kann in den Schleifenrumpf
auch ein if einbauen, sollte das relativ klar sein, warum das jetzt
hier das korrekte Ergebnis ist.
Die kleine Sternchenfrage, nehmen wir mal an, ich hätte jetzt hier,
wollte ich kein Integer Array nehmen, sondern ein Double Array.
Und ich nehme also auch hier wieder ein Double Number.
Was müsste ich denn dann alles ändern?
Einfach mal aufzeigen.
Wenn einer die Lösung hat.
Wenn ich die vergleiche, also wenn ich jetzt zählen will, wie viele
Doubles Number tauchen auf in einem Double Array A.
Was muss ich alles ändern in dem Programm?
Also offenbar die Signatur, das ist klar, das habe ich ja schon
verraten.
Hier müsste Double Array A stehen und Double Number, aber was muss ich
sonst noch ändern?
Ja, bitte.
Den Rückgabetyp, sehr richtig, genau.
Das ist, nee, das ist Quatsch, weil das ist weiter ein Int, sorry.
Nein, Sie müssen aber noch was anderes ändern.
Was muss man, ja.
Ganz genau, sehr richtig.
Ich sage es nochmal laut für alle, der Vergleich muss geändert werden.
Sie vernehmen hier gleich, gleich und gleich, gleich klappt für
Integers super.
Aber, Sie erinnern sich an die Vorlesung bei mir über Gleitkomma
Arithmetik, aufgrund von Rundungsfehlern kann es sehr häufig sein,
dass Sie eigentlich denken, das sollten gleiche Werte sein.
Sie unterscheiden sich aber um ein kleines Epsilon.
Das heißt, man nimmt auch nicht den Gleich-Gleich-Operator, sondern
was man in der Regel verwendet, ist eine Routine, die eben den
Absolutbetrag nimmt und festlegt, der muss unter einem Schwellwert
liegen.
Und dann könnte man stattdessen den Vergleich machen und genauso
zählen.
Das geht zwar syntaktisch auch, Gleich-Gleich ist auch auf Doubles
definiert.
Nur in der Regel ist es nicht das, was Sie im Hinterkopf haben, wenn
Sie das hinschreiben.
So, genau.
Und hier das Integer bleibt natürlich, weil das ist die Laufvariante,
das muss ein Integer sogar sein, wie wir eben gelernt haben.
Sie können nicht mit einem Double ein Array referenzieren, das geht
nicht.
Dann, ähnliche Aufgabe.
Jetzt möchte ich nach einer Zahl in einem Integer-Array suchen und
will im Prinzip hier von meinem Array A wissen, ist die Zahl Number
darin enthalten.
Und ich mache keine weiteren Annahmen über das Array A, also
insbesondere nicht, dass das sortiert vorliegt oder so.
Und Sie ahnen es, dann habe ich Pech, ich kann nichts anderes machen,
als das Ding nacheinander durchzugehen und zu schauen, ist jetzt da
meine Number drin.
Auch hier wieder, ich habe Glück, es sind Integers, ich kann Gleich
-Gleich hinschreiben ohne Bauchweh.
Und was ich jetzt hier mache, ist eigentlich so ähnlich wie oben.
Ich schaue mit dem If nach, ist es gleich, dann mache ich einen Break.
Was mache ich mit dem Break?
Ich verlasse die Schleife.
So, und das heißt, es gibt zwei Gründe, warum ich am Ende der Schleife
sein kann.
Der eine Grund, warum ich am Ende der Schleife bin, ist, dass ich
einfach alles durchiteriert habe und das If nie durchgeführt wurde.
Dann hat I den Wert A Punkt längs.
Ich habe das I bis dahin durchiteriert, die Schleife hat abgebrochen,
weil I nicht mehr kleiner war als A Punkt längs, sondern
logischerweise I dann den Wert von A Punkt längs hatte.
Und in dem Fall ist also die Zahl nicht drin.
Deswegen vergleiche ich das auch auf Ungleichheit, was selber heißt
wie nicht gleich, logischerweise.
In allen anderen Fällen, wenn ich hier das Break ausgeführt habe, habe
ich die Schleife verlassen, aber das I hat auch tatsächlich einen
anderen Wert wie längs, weil ich ja offenbar nicht bis zum Ende der
Schleife kam.
Und insofern ist es ungleich.
Das heißt, ich gebe dann true zurück, weil es ungleich ist.
Also ich werte diesen boolischen Ausdruck aus und habe dann
tatsächlich auch gesagt, ja, schaut mal, es ist vorhanden, weil ich
habe ja das Break einmal ausgeführt, also es war vorhanden gewesen mit
gleich gleich.
Jetzt werden Sie sagen, das ist aber ein bisschen knifflig.
Ich habe Ihnen das trotzdem so hingeschrieben, weil man das relativ
häufig sieht, dass man hier quasi einen logischen Ausdruck hat, den
man auch noch mal perverserweise invertiert hat.
Ja, aber das geht natürlich.
Und Sie damit im Prinzip abprüfen, würden Sie die Schleife komplett
durchlaufen oder vorher verlassen.
Sie hätten in dem Falle, weil danach eh nichts mehr berechnet werden
muss, auch anders das schreiben können.
Hat einer eine Idee, wie man ohne die Break-Kiste auch das hätte
schreiben können?
Ja?
Genau.
Ich wiederhole es nochmal für alle, war aber genau richtig.
Man könnte natürlich hier schon auch einen Return reinschreiben, und
zwar einen Return true.
Und dann schreiben Sie hier einfach Return false.
Weil wenn Sie hier an diese Stelle kommen, dann gab es wohl offenbar
nie die Gleichheit, ist dasselbe, dann ist es offenbar nicht drin
gewesen.
Kann man auch machen.
Fände ich ein bisschen eleganter, weil man vielleicht diese extra Spur
des Überlegens, die ich Ihnen eben erzählt habe, warum stimmt denn das
jetzt, sich dann spart und relativ offensichtlich das dokumentiert
hat.
Also das wäre sogar noch die leichter lesbare Variante.
Aber ich wollte Ihnen auch, weil man das auch manchmal sowas sieht,
auch nochmal hier erklären.
So.
Jetzt schauen wir uns nochmal gerade Arrays von Objekten an.
Und zwar an dem Beispiel, wir wollen einen kompletten Polygonzug.
Wir definieren Polygone.
In der Fläche können Sie sich jetzt erstmal vorstellen, wie
umgangssprachlich gesprochen, Linien, die aber verbunden sind.
Und jetzt haben wir hier, wir können ja jede dieser Linie quasi als
Vektor darstellen.
Und jetzt können wir im Prinzip sagen, ich habe einen Polygon, der
besteht aus einem Array von 2D Vektoren und die heißen Vertices.
Entschuldigung.
Und wir haben das einmal als Attribut hier im Konstruktor auch und
dann machen Sie hier diese Zuweisung.
Achtung, Achtung, Achtung, Achtung.
Sie sollten sich bewusst sein, Sie machen eine Zuweisung von einem
Array.
Das heißt wieder, es wird nichts kopiert.
Das, was Sie hier von außen als aktuellen Parameter beim Aufruf mit
reingeben, darauf wird Vertices zugewiesen.
Sie rufen ja hier nicht nochmal New auf und erzeugen eine neue
Objektidentität, sondern Sie weisen die Objektidentität, die Sie von
außen reinbekommen, diesem Vertices zu.
Das ist wichtig, dass Sie das auch wieder verstanden haben.
Das ist dasselbe eben wie beim Aufruf von aktuellen und bei der
Konversion zu formalen Parametern gewesen bei Klassen.
Es wird keine Objektidentität erzeugt, dasselbe gilt auch für Arrays.
Sie haben nicht nochmal New Array aufgerufen, oder nein?
Sie reichen hier quasi nur eine Objektreferenz weiter.
Vorteil ist, es geht schnell.
Einen Speicherzugriff, zack, fertig.
Sie brauchen nicht ein Riesenarray kopieren, was wirklich endlos viel
Arbeit wäre.
Für die C-Programmierer unter Ihnen, nochmal gerade so einen kleinen
Einstein -Ausflug am Ende, in C ist das witzigerweise ein bisschen so
schlampig gemacht worden auch so.
Normalerweise haben Sie in C auch das, was man als Experte einen
Wertaufruf nennt.
Das heißt, Sie kopieren den Wert.
Aber man hat dann bei C gesagt, Arrays kopieren, das ist so viel
Arbeit, das dauert so lange, das machen wir nicht.
Da wird einfach die Referenz dann weitergegeben.
Egal, ob Sie es deklariert haben in C oder nicht, der Compiler dichtet
es dran.
In Java ist es im Sprachstandard auch mal so definiert worden.
Und es ist eigentlich auch logisch, nach dem, was ich Ihnen erzählt
habe, hier, das ist im Prinzip erstmal nur eine Referenz.
Und die Referenz wird kopiert, also sauber.
Aber wie gesagt, Sie müssen da das Ergebnis denken.
Wenn Sie jetzt nachträglich den aktuellen Parameter hiervon
manipulieren, haben Sie auch Sys.Vertices manipuliert, weil es einfach
dieselbe Identität dahinter ist.
Genau wie bei Objekten.
So, und dann haben wir noch eine Methode hier, dass wir sagen, ich
möchte zum Beispiel die Zahl der Vertices haben.
Und dann könnte man Vertices.Length zurückgeben.
Was im Prinzip nichts anderes ist, wie eben die finale Variable von
einem Vertices Array.
Sie müssen sich hier an der Stelle keine Gedanken machen, weil Sie
sagen vielleicht, oh, length ist doch final.
Hier, das ist ja nicht als final deklariert.
Macht aber nichts, nach Methode können Sie eh nichts zuweisen.
Also Methode ist eh nur, wenn es einen Rückgabetyp hat, auslesbar.
Ist also alles korrekt hier.
Gut, dann, wie kann man das jetzt erzeugen?
Das machen wir jetzt noch als Abschluss, dann sind wir fertig.
Variante 1 ist natürlich, Sie erzeugen sich hier erstmal das Array mit
new Vector5.
Jetzt haben Sie 5 Objektreferenzen sich erzeugt.
Und dann fangen Sie an, die alle einzeln zu füllen, natürlich, weil
Sie neue Objektidentitäten jetzt wollen für jedes Feld.
Jedes Mal auch ein new Aufruf.
Und dann am Ende haben Sie im Prinzip Ihr Array von Vertices und dann
können Sie jetzt sagen, jetzt erzeuge ich mir meinen Polygon mit
diesen Vertices.
Variante 2 ist, weil man sagt, naja, eigentlich brauche ich ja dieses
Vertices am Array hier gar nicht mehr.
Ich will es ja eigentlich nur erzeugen für diesen einen Aufruf.
Und dann kann man das in Java auch so machen, dass man das komplett
anonymisiert.
Das heißt, Sie sagen halt, ich habe meinen Polygon, 2D-Polygon, 2D
erwartet ein Array von Vektoren, bekommt es hier, new Array.
Und die initialisiere ich gleich statisch mit diesen Vektoren, die ich
auch hier erzeuge.
Das heißt dann anonym, weil Sie keine Objektnamen hier vergeben.
Sie haben ja vorher hier den Objektnamen Vertices vergeben.
Das machen Sie hier alles gar nicht.
Und insofern ist das quasi, wenn man so will, anonyme Klassen, die Sie
hier erzeugen.
Aber es ist unter Umständen natürlich ein bisschen eleganter, weil man
damit dem Programmierer vielleicht nochmal deutlicher macht, das
erzeuge ich hier nur für diesen einen Aufrufkontext.
Wohingegen hier weiß ich ja nicht, ob Vertices nochmal woanders da
sind.
Vor allem das Schlimme ist, wenn ich diesen Bezeichner Vertices habe
und den von außen ändere dann später, dann ändere ich auch diesen
Polygonzug.
Das Problem habe ich hier nicht.
Ja, hier habe ich nichts mehr, was von außen manipulierbar ist.
Und das ist im Zweifel eher das, was Sie wollen.
So, jetzt haben wir noch eine Minute.
Für das Quiz ist es ein bisschen knapp, deswegen machen wir hier
Schluss und dann beim nächsten Mal weiter.
Vielen Dank.