So, dann auch heute wieder willkommen zur Programmieren-Veranstaltung.
Diesmal von etwas näher als sonst.
Aber Sie wissen ja schon, nächstes Jahr werde ich dann die Vorlesung
nicht weiterhalten, sondern der Herr Dr. Burger wird das machen, weil
sich bei uns eben der Nachwuchs angekündigt hat, irgendwann Ende des
Jahres, Anfang nächsten Jahres.
Und dann ist es keine so eine gute Idee, dann wahrscheinlich noch
Vorlesung zu halten.
Das ist jetzt auch der Grund, warum ich mich jetzt hier mal hingesetzt
habe.
Ach, eineinhalb Stunden stehen muss auch nicht sein.
Genau, aber heute bin ich noch da und wie gesagt, nächstes Jahr macht
dann Herr Burger weiter mit den letzten, weiß nicht wie viele sind es
noch, sechs?
Weniger wahrscheinlich, sogar Vorlesungen dann im neuen Jahr.
Genau, heute schauen wir mal, wäre mein Plan eigentlich, drei Themen
uns anzuschauen.
Wir haben nämlich jetzt zum einen noch das Thema Exceptions für uns,
wo wir ja beim letzten Mal schon ein bisschen angefangen motiviert
hatten.
Und dann zwei kürzere Foliensätze zum Thema Rekursion und Java API.
Jetzt fällt mir gerade ein, ich habe die Foliensätze noch relativ
kurzfristig online geändert, also geändert und meinen Mitarbeiter
geschickt, ob die jetzt auch tatsächlich schon online sind.
Die letzten Versionen habe ich jetzt gar nicht mehr nachgeschaut, muss
ich sagen.
Also zumindestens hier sehen Sie schon mal so eine Version 1.1, habe
ich mal diesmal dran gedacht, auch hochzuzählen, falls Sie dann noch
einen anderen Ausdruck haben.
Sind aber nur Detailänderungen, also selbst wenn Sie jetzt was Älteres
vor sich liegen haben, sollte das auch kein Problem sein.
Einfach ein paar Fehlerchen, die mir noch aufgefallen sind, so beim
letzten Durchschauen.
Genau, okay.
Also, erstes Thema, Exceptions, wo wir eben schon motiviert hatten,
das ist ein Weg, um mit Ausnahmesituationen im Programmcode umzugehen
und um gleichzeitig auch explizit zu machen, was ist quasi normaler
Ablauf, wie man es erwartet und was sind Ausnahmesituationen und im
Prinzip dieses Behandeln des normalen Ablaufes und der
Ausnahmesituationen voneinander zu trennen, um einfach Code
verständlicher zu machen und gleichzeitig auch den aufrufenden Klassen
zum Beispiel klar zu machen, was für Ausnahmen auftreten können, damit
die sich wiederum in ihrem Programmcode darauf einstellen können, auf
diese fehlgeschlagenen Sachen.
Beispiele für Exceptions oder Fehlschläge, die Ihnen recht häufig
begegnen werden, sind solche Dinge, wie man öffnet eine Datei, die es
gar nicht gibt, also der Pfad zu der Datei ist nicht richtig
angegeben, man versucht auf Array-Elemente zuzugreifen, die gar nicht
da sind, also außerhalb der Indexgrenzen, man versucht durch Null zu
teilen oder man greift allgemein irgendwie auf eine Null-Referenz zu.
Genau, auf den nächsten beiden Folien, die hatten wir ja auch schon
besprochen, ging es einfach mal darum zu zeigen, was passiert dann,
wenn man Fehlerbehandlung einfach nur in den Code irgendwie hinein
mischt, dann kommt man ganz schnell zu relativ unübersichtlichen
Kontrollflüssen, kann man sich anhand dieser beiden Beispiele eben
anschauen, viele If-Statements und man weiß eben nicht, was ist denn
hier jetzt eigentlich der Punkt sozusagen.
Sowieso mit Gotos würden wir jetzt nicht arbeiten, wie in diesem
Beispiel, aber auch wenn man das jetzt in strukturierter
Programmierung mit eben If-Abfragen macht, wird das nicht gerade
übersichtlicher, das Ganze.
Andere Alternative, wie man es machen könnte, aber nicht machen
sollte, ist zu sagen, Fehler behandle ich immer nur lokal und gebe
irgendwie eine Warnung an den Benutzer aus und versuche dann aber
weiterzumachen.
Dann hätte ich eben gerade das Problem, dass auch der Aufrufer von der
Klasse nicht reagieren kann auf den Fehler, wenn das nur auf der
Konsole ausgegeben wird.
Oder, dass ich eben auch Programmlogik und Fehlerbehandlung dann
vermische miteinander.
Genau.
Das ist also alles zur Motivation, wie man es nicht machen sollte.
Was aber gleichzeitig das Problem ist, dass ich eben schon mit
irgendwelchen schon vorhergesehenen Ausnahmen, aber eben nicht im
normalen Ablauf eigentlich vorkommenden Ausnahmen, wie ich damit
umgehe.
Und in Java ist die Antwort dann eben sogenannte Exceptions zu
verwenden, vom Namen her eben auch schon Ausnahmen, um sowas explizit
ausdrücken zu können, dass es hier gewisse Ausnahmesituationen geben
kann.
Zu dieser Folie Exceptions behandeln also Fehlersituationen im
Programmablauf, werden zur Laufzeit überprüft, ob diese
Fehlersituationen vorliegen, und wenn sie vorliegen, wird der normale
Kontrollschluss sozusagen unterbrochen und dieser Fehler wird, oder
diese Ausnahme wird erzeugt und an anderer Stelle, was man dann eben
ein bisschen getrennt voneinander halten kann, kann diese Ausnahme
dann behandelt werden.
Typische Verwendung von so einer Exception wäre dann also irgendwo in
meinem Kontrollschluss tritt ein Problem auf, was von dem normalen
Ablauf, so wie ich mir den gedacht habe, abweicht und sodass es dann
auch keinen Sinn macht, den Algorithmus, wie er geplant war,
fortzuführen.
Wie gesagt, wenn man zum Beispiel aus einer Datei irgendwelche
Informationen einlesen will und die Datei ist gar nicht da, dann macht
das keinen Sinn, wahrscheinlich irgendwie normal fortzufahren.
Das geht dann nicht.
Lokal darauf reagieren könnte ich in so einem Fall dann nicht, sondern
die Behandlung des Problems wäre dann an einer anderen Stelle
erforderlich.
Ich würde zum Beispiel den Kontrollschluss zurückgeben an die Stelle,
wo mit dem Benutzer interagiert wird und dem dann zum Beispiel eine
Warnung anzeigen, dass der angegebene Dateipfad nicht der richtige ist
und er doch bitte nochmal neuen angeben soll, oder sowas.
Ein Beispiel.
Genau.
In Java wird das umgesetzt auch als Objekte.
Also Exceptions sind Objekte, die diese Ausnahmesituation sozusagen
charakterisieren, die Informationen zu diesem aufgetretenen Fehler
enthalten.
Die kann ich also wie jedes andere Objekt mit new erzeugen und habe
dann aber noch ein spezielles Schlüsselwort für dieses Unterbrechen
des Kontrollschlusses, nämlich dieses Schlüsselwort throw hier an der
Stelle.
Man kann das dann verwenden, wie hier gezeigt.
Wenn wir also eine Methode hätten, um den Monat zu setzen von einem
Datum zum Beispiel, könnte man als erstes mal prüfen, also hätte man
hier als Eingabeparameter den Monat als Integerwert angegeben und da
gibt es natürlich ziemlich viele mögliche Werte, die keinen sinnvollen
Monat ergeben.
Also könnte man dann eine Abfrage machen.
Ist dieser Parameter Monat und der aktuelle Wert dieses Parameters
zwischen inklusive 1 und 12?
Und falls das nicht der Fall ist, würde das eben an dieser Stelle
heißen, ich kann nicht sinnvoll weitermachen.
Eine Set-Methode eines Monats, vielleicht würde dahinter dann auch ein
Switch -Case kommen, was wirklich über die einzelnen Monate rübergehen
würde.
Macht keinen Sinn, wenn dieser Monatsparameter nicht zwischen 1 und 12
ist.
Also erzeuge ich eine solche Ausnahme.
Man macht das dann also so.
Man sagt, es gibt eine neue Exception von einem bestimmten Untertyp
der Exception.
In diesem Fall Illegal Argument, dass ich also sage, diese Methode
wurde mit falschen Parameter aufgerufen.
Kann dann auch eine Erklärung übergeben als Parameter an diesen
Konstruktor.
Das ist eben ein Konstruktor.
Ich erzeuge ein solches neues Objekt, was mir diese Ausnahmesituation
beschreibt und kann das dann an der Stelle werfen.
Und wie gesagt, Throw bedeutet, der Kontrollfluss wird an der Stelle
unterbrochen.
Ähnlich wie beim Return, wo es eben ausnahmsweise unterbrochen könnte
man vielleicht sagen.
Und falls das nicht der Fall ist, also falls diese If-Bedingung hier
jetzt nicht zu treffen würde, würde ich halt hier unten entsprechend
mit der Ausführung der Methode weitermachen.
In dem Fall jetzt nur noch den Monat setzen.
Genau, jetzt haben wir hier schon gesehen, das ist eine speziellere
Ausnahme.
Also sieht man jetzt hier nicht direkt, aber es ist ein Subtyp von
dieser allgemeinen Klasse Exceptions.
Es gibt eben in Java eine allgemeine Oberklasse, um solche Ausnahmen
zu beschreiben.
Die heißt Exceptions.
Es gibt dann eine Menge von davon abgeleiteten, vorgefertigten
Exceptions, wie zum Beispiel diese Illegal Argument Exception, die man
verwenden kann.
Null Pointer Exception, die Ihnen bestimmt schon mal begegnet ist,
wäre so ein anderes Beispiel.
Also gibt es eine ganze Reihe von Klassen, die Java schon zur
Verfügung stellt.
Exception Klassen, die man auch selber werfen kann und noch öfter
wahrscheinlich fangen muss.
Aber es ist auch möglich, eigene Exception Klassen zu definieren.
Wenn man selber komplexere Programme schreibt, macht es auch Sinn, für
die speziellen Ausnahmesituationen, die man vorfindet, auch eigene
Exception Klassen zu definieren und die dann eben wiederum zu
behandeln.
Die werden dann auch von der Exceptions Klasse abgeleitet,
beziehungsweise womöglich von einer spezielleren, muss man gucken.
Und solche eigenen Exceptions Klassen müssen dann mindestens ein
Default -Konstruktor haben und auch einen Konstruktor mit einem String
-Parameter haben, so wie wir es auf der Folie vorher gesehen haben,
dass man eben die Begründung, was denn schiefgegangen ist, nur als
Nachricht, als Message mit übergibt und eben zusätzliche Informationen
zu den Fehlerumständen dann übergibt.
Das muss man also, ja, hier diese Dinge muss man bedenken, wenn man
selbst so eine Exception Klasse implementieren möchte.
Wenn man andersrum mit den Exceptions auch mit den Vorgefertigten
arbeiten möchte, sind auch wichtige Methoden dieser Klasse zum einen
diese Methode GetMessage, da würde man nämlich gerade diese
Fehlernachricht, die man beim Erzeugen einer Exception reingeben kann,
dann wieder auslesen an anderer Stelle.
Die Methode PrintStackTrace ist dafür da, den Aufruf-Stack auszugeben,
der zu dem Zeitpunkt, als die Exception erzeugt wurde, vorlag.
Also in dem Beispiel hier würden quasi die ganzen Methoden, die zum
Aufruf dieser Methode geführt haben, in diesem StackTrace drinstehen
und als letztes, als oberstes Element im StackTrace würde diese
Methode SetMonth stehen.
Also würde da klar werden, aha, diese Exception, dieses Objekt
Exception wurde irgendwo innerhalb dieser Methode SetMonth erzeugt.
Also von daher noch ganz praktisch, um eben das eigene Programm zu
debuggen, wenn man eine NullPointerException plötzlich außen am
Programm sozusagen beobachtet, kann man anhand des StackTraces dann
sehen, wo kamen die dann her, oder kann sich in der Main-Methode
erstmal ausgeben lassen, wo kam diese NullPointerException dann her,
oder wird ja eigentlich auch schon automatisch ausgegeben.
Genau, das ist einfach die Ausgabe des Ausbruchs-Stacks auf die
Konsole, man kann dann auch noch in der, wenn man jetzt detailliertere
Fehlerbearbeitung machen möchte, sich auch ein Array von solchen
StackTrace -Elementen zurückgeben lassen und das also auch
programmatisch analysieren, was da passiert ist, um vielleicht noch
Gegenmaßnahmen einleiten zu können, vielleicht eine andere Dateipfad,
ein Default-Dateipfad zu verwenden, in dem Fall, dass die Datei nicht
gefunden wurde zum Beispiel.
Genau, das sind so die drei wichtigen Methoden dieser Exceptions
-Klasse.
Es gibt einige, wie gesagt, Unterklassen, die von Exception sowieso
schon zur Verfügung gestellt werden, von Java, eine davon ist der
Error, Moment, oder ist der eigentlich sogar selbst daneben stehend?
Ne, es ist sogar daneben stehend neben der Exception, also es gibt
auch schwerwiegendere Fehler, nicht nur Ausnahmen, sondern regelrechte
Fehler, Error.
Da ist dann der Unterschied, ja, das verwendet man für schwerwiegende
Fehler, sagt man, also nicht Fehler, die vom Aufrufer einer Methode
wiederum korrigiert werden könnten, sondern für schwerwiegende Fehler,
wo man eigentlich davon ausgeht, dass das Programm beendet werden
muss, dass da nichts Sinnvolles mehr passieren kann, wo man aber eben
trotzdem explizit machen will, gegenüber dem Benutzer oder vielleicht
auch irgendwelchen Logs oder so, wo das und was da passiert ist.
Verwendet man von daher eigentlich eher seltener.
Diese Hierarchie, die von Java eben vorgegeben wird, in Sicht der
Exception sieht dann wie folgt aus, es gibt, wie gesagt, die
Exceptions, die man dann auch wieder fangen und behandeln kann, wo das
Programm vielleicht sich also selbst noch wieder erholen kann,
sozusagen von dieser Ausnahmebedingung und was anderes versuchen kann,
und die Oberklasse Error für eben schwerwiegende Fehler, wo es
eigentlich nur darum geht, den Benutzer zu informieren.
Die leiten beide ab von dem Obertyp Throwable.
Das sind also Klassen, also alle Int-Klassen davon sind welche, die
man mit dem Schlüsselwort Throw eben werfen kann.
Das ist ja das Analog Throw Exception.
Bei der Exception-Klasse selber gibt es nochmal zwei wichtige
Unterscheidungen oder eine, sagen wir mal, spezielle Unterklasse, das
ist die Runtime Exception und sozusagen alle anderen Exceptions.
Das sind hier beispielhaft einfach mal die IO Exception, auch als eine
vorgefertigte Exception von Java genannt, aber hier könnte man dann
zum Beispiel auch noch eigene Exceptions drunter hängen oder sehr
viele andere von Java vorgegebene Exceptions hängen dann sozusagen
auch hier neben der Runtime Exception.
Die Runtime Exception ist speziell, das sehen wir aber im Prinzip
gleich auch erst, weil das Exceptions sind, wo man nicht garantieren
muss, dass sie auch gefangen werden sozusagen.
Also Runtime Exceptions sind welche, die in der Signatur einer Methode
nicht vorkommen müssen, wenn die Methode die werfen kann und alle
anderen Exceptions, sehen wir gleich noch, müssen auch in der Signatur
einer Methode vorkommen.
Genau, um das hier schon mal in der Hierarchie zu sehen, das ist also
eine spezielle Art von Exceptions, wo es dann eben auch wieder von
Java vorgegebene Unterklassen gibt, wo es eigentlich weniger Sinn
macht, da eigene Unterklassen zu definieren, sondern eigene
Unterklassen würde man also hier neben sozusagen definieren.
Beispiele für solche Runtime Exceptions wären eben Null Pointer, die
Ihnen bestimmt, wie gesagt, schon begegnet sind in den Übungen, Index
out of Bound, wenn ich eben auf ein Array versuche, verkehrt
zuzugreifen außerhalb der Indexgrenzen, oder Arithmetic Exception wäre
noch so ein anderes Beispiel.
Genau, auch hier gibt es noch von Java vorgegebene weite Hierarchien,
also es gibt so eine Oberklasse IO Exception für Input Output
Exception und was Spezielleres wäre dann sowas wie eine File Not Found
Exception, das ist auch schon was von Java einfach vorgegeben ist, was
man dann auch nicht manchmal neu definieren muss.
Ja, auf der Arrow-Seite sehr viel weniger häufig, also sollte Ihnen,
denke ich mal, noch nicht begegnet sein, in den jetzigen
Übungsaufgaben, wirklich schwerwiegende Fehler, es kann sowas sein,
wie dass die Java Virtual Maschine keinen Speicher mehr verfügbar hat
und deswegen abbrechen muss, dann würde man so einen Out of Memory
Error bekommen.
Da gibt es dann eben nicht mehr viel, was man machen kann, weil dann
auch der Zustand des Speichers irgendwie undefiniert sein könnte, dann
an dem Zeitpunkt.
Okay.
Genau.
Jetzt haben wir darüber gesprochen, oder was zum einen die Hierarchie
von Exception ist und auch schon gesehen, wie man diese Exceptions
auslösen kann und ich habe schon immer mal ein bisschen angedeutet, es
geht natürlich nicht darum, nur Exceptions auszulösen und die bis nach
außen zum Benutzer zu werfen, sozusagen, sondern natürlich auch darum,
bei Exceptions an geeigneter Stelle im Programm wiederum
Fehlerbehandlung einzubauen.
Also, wie gesagt, wenn wir jetzt das Beispiel wieder hernehmen, dass
ich eine Datei nicht finden kann, wäre eine geeignete Stelle
wahrscheinlich, dem Benutzer nochmal irgendwie ein Eingabefeld
anzuzeigen, wenn man wirklich auch eine GUI hat, und nochmal Bescheid
zu geben, die Datei existiert nicht und ihn aufzufordern, eine andere
Datei zum Beispiel anzugeben.
Wäre so ein Beispiel.
Und diese Stelle, die also auf den Fehler reagiert, ist die Stelle, an
der man die Fehlerbehandlung betreibt, also das nennt man dann
Fehlerbehandlung an der Stelle, und das macht man in Java mittels
sogenannter Try-Catch-Löcke.
Also man hat erstmal einen Bereich des Programmcodes, der von so einem
Try umfasst wird, und das bedeutet, dass alle Exceptions der
entsprechenden Typen, die innerhalb dieses Blocks erzeugt werden,
geworfen werden, dann hier hinten nach dem Try gefangen werden können.
Also es ist wirklich diese Analogie, ich werfe eine Ausnahme, und
irgendjemand fängt sie dann wieder Catch.
Das heißt, hier in der Mitte in dem Try-Block konnten also
irgendwelche Exceptions auftreten, und diese Catch-Löcke werden dann
ausgeführt, wenn eben der passende Typ von Exception innerhalb dieses
Blocks zur Laufzeit aufgetreten ist.
Beispiel wäre hier unten, daran kann man es vielleicht besser
erkennen, hier hat jetzt also ein Try-Block, wo ich versuche, auf eine
Testdatei, namens test.txt, zuzugreifen, und da die einzelnen Zeichen
rauszulesen, mit einem File-Reader, darauf kommt es jetzt nicht an,
aber ich würde hier halt auf diese Datei zugreifen, und dann
schrittweise die Zeichen aus der Datei lesen.
Würde das hier versuchen, und könnte das hier abschließen, mit zwei
möglichen Catch-Blöcken, nämlich einmal dem Fall, dass ich hier sage,
falls hier eine File-Not-Found-Exception erzeugt wird, fange ich die
hier auf und behandle den Fehler, indem ich einfach ausgebe, Datei
wurde nicht gefunden.
Oder, falls eine IO-Exception im Allgemeinen, das ist ja eine
Oberklasse, auftritt, würde ich eine Ausgabe machen, es gab irgendwie
einen Ausgabefehler.
Das ist jetzt nicht wirklich eine besonders ausgefuchste
Fehlerbehandlung, im Sinne von, dass ich mich von diesem Fehler
erhole.
Hier als Beispiel, ich gebe halt aus und beende dann das Programm
trotzdem, aber als Beispiel, dass man sieht, wie da der Kontrollfluss
passiert.
Wenn es jetzt also hier oben zu einer File-Not-Found-Exception kommt,
geht die Animation, glaube ich, noch weiter, genau, wird der
Kontrollfluss hier oben abgebrochen, das heißt, diese paar Zeilen
werden dann gar nicht erst ausgeführt, sondern der Kontrollfluss
springt dann gleich an das Ende des Trial-Blocks zu dem ersten Catch
und überprüft dann eben in dieser Reihenfolge, ob die entsprechende
Exception hier geworfen wurde, würde dann hier gleich feststellen,
aha, hier kam eine File-Not-Found-Exception, das heißt, ich kann dann
gleich den ersten Catch-Block auch betreten.
Geht's hier noch weiter?
Nee.
Und dann wird eben dieses System.out.println ausgeführt, falls dieser
Konstruktor hier eine solche File-Not-Found-Exception wirft oder
erzeugt.
Genau.
Das hatte ich jetzt im Prinzip auch schon gesagt.
Was passiert also mit dem Kontrollfluss?
Wir haben aber gleich nochmal ein Beispiel.
Programmfluss wird also an der Stelle unterbrochen, wo die Exception
auftritt oder auch weitergegeben wird und ich kann dann im Aufruf
-Stack nach umgebenden Try-Catch-Blöcken suchen und den Catch-Block
ausführen, den ersten Catch-Block ausführen, für den gilt, dass der
deklarierte Typ ein Obertyp des Ausnahmetyps ist.
In diesem Fall hatten wir hier einen direkten Match.
Hier stand, hier wird eine File-Not-Found-Exception erzeugt und hier
im Catch steht auch direkt eine File-Not-Found-Exception drin.
Wenn jetzt dieses erste Catch hier nicht stehen würde, würde das
zweite dann auslösen, weil eine File-Not-Found-Exception eben auch
eine IO-Exception ist.
IO-Exception ist eine Oberklasse von File-Not-Found-Exception.
Genau.
In dem Fall, wo es aber genau hier so geschrieben ist, würde eben der
erste passende Catch-Block, also dieser hier, ausgeführt werden und
der zweite dann auch nicht mehr.
An der Stelle.
Genau, also der erste Catch-Block, für den gilt, deklarierter Typ ist
Obertyp des Ausnahmetyps und wenn ich dann diesen Catch-Block
abgearbeitet habe, wird fortgesetzt nach dem letzten Catch dieses
MyCatch -Block.
Auch hier wieder ein Beispiel, wenn wir jetzt also diese, auch jetzt
mal über mehrere Methoden verteilt, eine Situation haben, dass ich
hier eine Methode foo habe, in der unter irgendwelchen Bedingungen
eine IO-Exception geworfen werden kann.
Diese Methode foo wird aufgerufen von der Methode bar hier im Beispiel
und die Methode bar wird aufgerufen von der Methode bus hier im
Beispiel.
In unserem Beispiel hätten wir jetzt hier, genau, hier das Werfen
einer neuen Exception, sehen wir dann gleich auch noch, dann muss die
Methode hier deklarieren, dass diese Exception geworfen werden könnte.
Hier unten in der zweiten Methode wird diese Methode einfach nur
aufgerufen, keine Fehlerbehandlung gemacht in diesem Beispiel.
Das heißt, auch diese Methode muss in ihrer Signatur enthalten, dass
diese IO-Exception geworfen werden könnte.
Und in der letzten Methode hier hätten wir eben jetzt ein Beispiel für
so ein Try-Catch-Block, dass innerhalb eines Try-Blocks die Bar
-Methode aufgerufen wird und dahinter dann ein entsprechender Catch
-Block für eine IO-Exception vorkommt.
Was würde jetzt also passieren, wenn diese Bedingung hier, wenn wir
zum einen die Methode aufgerufen haben und diese Bedingung hier wahr
wird, dann wird eben die Exception erzeugt, das Objekt, und geworfen.
Das heißt, der Kontrollfluss wird hier abgebrochen, die Methode wird
hier beendet.
Das hier unten wird nicht mehr ausgeführt.
Und der Kontrollfluss springt hierhin eine Methode höher, würde jetzt
halt hier sozusagen suchen, gibt es hier ein Try-Catch um die Methode
herum.
Das ist nicht der Fall.
Dann würde man auch hier diese Methode direkt an der Stelle beenden
und nicht mehr den Rest, der da drunter kommt, ausführen.
Würde also wieder eine Methode nach oben im Stack, oder nach unten, je
nachdem, wie man sich das vorstellt, ergehen und hier ankommen.
Auch hier wieder gucken nach umgebenden Try-Catch-Blöcken, das wäre
dann dieser Try-Catch-Block.
Prüfen, ist der Typ der erzeugten IO-Exception hier im Catch irgendwo
enthalten?
Wenn ja, kann ich eben die Fehlerbehandlung an der Stelle ausführen
und dann ist das Weiterwerfen von Exceptions beendet an der Stelle.
Wenn jetzt hier kein Catch-Block passen würde, würde die Exception
wieder einfach weiter an den Aufrufer geworfen werden.
Also falls hier jetzt IO-Exception steht und hier unten wird aber eine
eigene Exception geworfen, dann würde der Catch-Block entsprechend
nicht greifen.
Genau, also dann gehe ich zum Catch und kann dann hier die
Fehlerbehandlung durchführen.
Genau, diesen Catch-Block, einfach noch zu den Begrifflichkeiten,
nennt man auch Exception-Händler.
Eben die Stelle, an der der Fehler behandelt wird, die Ausnahme
behandelt wird.
Das kann ich irgendwo im Aufruf-Stack machen und mach das dann eben
getrennt von dem normalen Programm-Code.
Das war ja das, wie wir es ursprünglich motiviert hatten.
Wir wollen das eben nicht vermischen, die Fehlerbehandlung und den
normalen Programm-Code.
Es gilt dann immer für jede Methode einzeln, dass ich entweder
Exceptions, die ausgelöst werden könnten, selbst behandeln muss mit so
einem Try-Catch-Block oder ich sie wiederum als Teil meiner eigenen
Signatur deklarieren muss mit so einer Throws-Klausel.
Wenn ich jetzt nochmal einen Schritt zurückgehe zu unserem Beispiel,
wäre das gerade hier.
In dieser Methode hat man sich jetzt also dafür entschieden, die
Exception nicht direkt zu behandeln, sondern ein Throws in die
Signatur hinzuzufügen.
Entsprechend hier ist dann also auch klar, die Foo-Methode könnte eine
IO -Exception werfen.
Das heißt, auch hier wäre die Entscheidung, mache ich jetzt ein Try
-Catch darum, um die Ausnahme hier zu behandeln oder werfe ich sie
selber weiter.
Auch hier die Entscheidung, sie weiterzuwerfen.
Und hier in dieser dritten Methode hat man sich dann eben dazu
entschieden, hier die Fehlerbehandlung dann auch zu machen.
In diesem kleinen Beispiel.
Okay, was heißt also deklarieren von Ausnahmen?
Das heißt einfach nur, dass sich Exceptions, die in meinem
Methodenrumpf auftreten können, sei es, weil ich sie selber mit Throws
werfe oder sei es, weil Methoden, die ich aufrufe, sie werfen könnten.
Genau, Exceptions, die in meinem Methodenrumpf auftreten können, muss
ich also entweder selber fangen mit Try-Catch oder selbst als Methode
wieder werfen.
Also meine Methode dann ergänzen, die Signatur meiner Methode
ergänzen, um dieses Throws und dann Exception, die Exception-Typen,
die auftreten können.
Sollte man möglichst speziell machen.
Man könnte hier immer nur einfach schreiben, Throws Exception, aber
das ist natürlich nicht besonders hilfreich dann für den Aufrufer, für
denjenigen, der diese Signatur verwenden will, deswegen sinnvoll da
immer so speziell wie möglich zu bleiben bei der Angabe, welche
Exceptions auftreten können.
Hier zum Beispiel also IO Exception und File Not Found Exception in
der nächsten Zeile.
Genau, das bedeutet oder führt einfach dazu, dass der Aufrufer einer
Methode eben als Teil der Signatur weiß, welche Ausnahmen er erwarten
muss und um welche Ausnahmebehandlung oder Fehlerbehandlung er sich
also entweder kümmern muss oder die Exception dann selbst weiter
werfen kann.
Das heißt, dieses Throws-Klausel, dieser Teil ist eben tatsächlich
Teil der Signatur einer Methode.
Und da kommen wir jetzt wieder zum Thema Runtime Exception, was ich
vorhin schon mal kurz angedeutet habe.
Es gibt eben auch diese eine Unterklasse von Exception, die nicht
deklarationspflichtig ist.
Das ist eben die Runtime Exception und die entsprechend davon
abgeleiteten Unterklassen und auch der Error ist nicht
deklarationspflichtig, weil Error ist jetzt wieder so eine Situation,
dass man sagt, das ist so ein schwerwiegender Fehler, kann ich sowieso
nicht behandeln, dann muss ich es auch nicht deklarieren.
Und Runtime Exception sind eben solche vielleicht auch generischen
Dinge, die auch irgendwo nicht vorhergesehen sind, sondern wirklich
auf Implementierungsfehler hinweisen, die einfach den Code sehr viel
stärker, ja, da müsste man im Prinzip an jede Methode fast dran
schreiben, dass da eine Runtime Exception passieren könnte.
Nicht ganz an jede, aber an ziemlich viele, deswegen spart man sich ja
das an der Stelle.
Genau, was ich gerade schon gesagt habe, in diesem Beispiel wurde
jetzt angegeben, IO Exception und File Not Found Exception und das
macht als Dokumentation auch Sinn.
Man müsste eigentlich nur IO Exception angeben, aber es ist, wie
gesagt, sinnvoll, trotzdem beide Exceptions anzugeben, um dann zum
Beispiel auch in den Kommentaren dieser Methode klarzustellen, in
welchen Situationen kann denn welche der Exceptions ausgelöst werden.
Genau.
Zu Error und Exception hatte ich auch ein bisschen schon gerade
gesagt, das hier nochmal, um das auch auf der Folie zu
verschriftlichen, gesagt, Error, ernsthafte Probleme, die nicht
vernünftig behandelt werden können, irgendwie der Bytecode ist kaputt,
die Java Virtual Machine ist kaputt, Schattenbug, was auch immer mal
passieren kann, muss deswegen auch nicht gefangen werden.
Diese Runtime Exceptions, die ich eben auch nicht deklarieren muss als
Teil meiner Signatur, deuten auf Programmierfehler dann hin, die eben
auch erst zur Laufzeit oftmals festgestellt werden können und meistens
das nicht ermöglichen, das Programm sinnvoll fortzusetzen.
Beispiel, Division durch 0 oder Zugriff auf ein 0-Objekt, Zugriff auf
ein unzulässiges Array-Element, das sind dann auch keine Ausnahmen wie
sowas, dass eine Datei nicht existiert.
Also, dass eine Datei nicht existiert, ist einfach eine Situation, mit
der ich als Programmierer rechnen muss.
Es ist nicht mein Standardfall, wie mein Ablauf passieren sollte, aber
es ist trotzdem ein Fall, den ich einfach abdecken muss.
Programmierfehler sind natürlich Fälle, die ich so erstmal nicht
vorgesehen habe, sind ja Fehler.
Also von daher noch eine andere Klasse von Ausnahme, und die ich dann
einfach, wenn sie dann doch mal in einem Test oder so auftreten,
dadurch reparieren sollte, dass ich denen die eigentlichen Ursprung
des Fehlers beseitige.
Also dann zum Beispiel feststelle, aha, ich habe irgendwo in meinem
Programm eine Stelle, wo ich auf Arrays zugreife, ohne vorher geprüft
zu haben, dass der Index, mit dem ich zugreife, auch innerhalb der
Grenzen liegt.
Das ist einfach typisches Vorgehen beim Arbeiten mit Arrays, dass ich
vorher, bevor ich auf das Array-Element zugreife, prüfe, passt der
Index dann auch, anstatt dass ich einfach mit dem ungeprüften Index
auf den Array zugreife.
Und falls ich deshalb doch mache letzteres, ist es ein
Programmierfehler, den ich dann direkt beseitigen soll.
Da muss ich dann also nicht Fehlerbehandlung für einführen, sondern da
sollte ich dann eher den Programmierfehler entfernen.
Genau, und sonstige Exceptions verwende ich eben für behandelbare
Programmfehler, wie Datei nicht vorhanden, Festplatte voll, Fehler
beim Parsen, was auch immer da die Fehlerursache sein könnte oder die
Ausnahme sein könnte.
Okay.
Haben wir auch schon ein bisschen angedeutet, welche Ausnahmen sollte
ich jetzt also in meinem Code behandeln.
Ähm...
Error und die Unterklassen ist per Definition nicht sinnvoll, die zu
behandeln, also da muss ich mich nicht drum kümmern und das kommt ja
auch an den Signaturen der Methoden, die ich aufrufe, nicht vor.
Ich sollte auch nicht allgemeine Behandlung machen, wo ich schreibe
Catch Exception.
Das ist zu allgemein und führt oftmals nicht dazu, dass man eine
sinnvolle Fehlerbehandlung an der Stelle machen kann, weil das kann
eben ganz viele Gründe haben, sondern ich wüsste eben, ich spring
jetzt mal zwei runter, die Unterklasse von Exception, die geeigneten
behandeln.
Außer, jetzt wieder nach oben springen, die Runtime Exception und
deren Unterklassen, auch da ähm, macht es im Allgemeinen nicht so viel
Sinn oder meistens nicht so viel Sinn, dafür Fehlerbehandlung
einzuführen, sondern man würde eher in den, wenn man so eine Exception
mal bekommt, gerade beim Testen des eigenen Programmcodes, dann eher
den darunterliegenden Programmierfehler beseitigen.
Und nicht einen neuen Catchblock dafür einführen.
Es mag ein paar Ausnahmen geben, also sowas wie eine Number Format
Exception, wenn man irgendwie ein neues Datum anlegt mit den
Standardklassen, die Java dafür bereitgestellt und da gibt es schon so
ein paar Ausnahmen, die auftreten können und die man auch sinnvoll
beheben kann.
Aber meistens würde man sagen, nein, lieber den Programmierfehler
behandeln.
Die anderen Unterklassen von Exception, die sind nicht durch Zufall,
auch gerade die, die man in der Signatur angeben muss, die sollte man
dann eben auch behandeln, beziehungsweise der Compiler zwingt einen ja
sogar dazu, diese Fehlerbehandlung dann auch zu machen.
Weil eben, wenn eine Methode ein Throws Exception von irgendeinem
Untertyp deklariert, würde der Compiler dann auch meckern, wenn man
selber nicht entweder in der eigenen Methode auch ein Throws
deklariert oder eben ein Try-Catchblock, ein passenden, einfügt.
Genau, das war eben zum Behandeln von Ausnahmen, zum Werfen von
Ausnahmen, welche Ausnahmetypen darf ich denn in meinem eigenen Code
auch auslösen, also werfen.
Man kann, wenn es denn wirklich nötig ist, Errors, beziehungsweise die
passenden von Java vorgegebenen Unterklassen oder sogar auch passende
eigene Unterklassen auslösen, mit der Bedeutung eben, dass irgendwas
passiert ist, von der sich das Programm eigentlich nicht erholen kann,
aber trotzdem will man ja dem Benutzer Bescheid geben.
Man kann auch Random Exceptions von Java vorgegebene oder auch eigene
auslösen, wenn sie denn tatsächlich zu dieser Ursache, die man da hat,
auch passen.
Und man kann Unterklassen von Exception auslösen.
Man sollte nicht einfach ThrowException schreiben, ohne weiteren Typ,
sondern dann entweder sich eine passende Unterklasse, die Java
vielleicht schon vorgibt, auswählen oder wahrscheinlich in vielen
Stellen im eigenen Programm noch sinnvoller sich eine eigene
Unterklasse definieren, die also genau diese Ausnahme, die hier
auftritt, dann auch beschreibt und weiter wirft.
Ein Beispiel für die Verwendung eines Errors, wobei ich jetzt auch
nicht damit sagen will, dass man es so unbedingt immer machen sollte,
aber um zu zeigen, für welche Ausnahmebedingungen wirklich Errors
gedacht sind, wäre was folgendes.
Ich habe ein SwitchCase-Statement, wo ich einen Monat überprüfe.
Ich habe vorher eigentlich schon eine Überprüfung, dass der Parameter
auch der richtige ist.
Falls nicht, würde ich so eine IllegalArgumentException ungültiger
Monat werfen, hätte dann hier so ein SwitchCase-Statement von 1 bis 12
und hier dann am Ende ein Default, der sagt, wenn ich hier jetzt
ankomme, was eigentlich nicht sein darf, dann werfe ich einen neuen
Error.
Dann ist also irgendwas in meiner Java Virtual Machine ganz schief
gegangen oder jemand hat mir, kann eigentlich auch nicht sein an der
Stelle, parallel irgendwie hier in meine Daten reingeschrieben.
Also im Sinne von das kann hier eigentlich nicht passieren und das
dokumentiere ich dann, indem ich sage, wenn ich hier ankomme, dann
muss irgendwie ein Bug in meiner Java Virtual Machine vorliegen, denn
das entspricht nicht der Sprachdefinition von Java.
Und da kann ich dann auch als Programm wahrscheinlich nicht mehr
geeignet drauf reagieren.
Aber wie gesagt, das jetzt mal als Beispiel, wann man so ein Error
vielleicht verwenden könnte.
Gleichzeitig heißt das nicht, dass man jetzt überall alle Stellen, wo
man eigentlich nicht ankommen sollte, mit so einem Error versieht.
Das ist wahrscheinlich auch ein bisschen übertrieben dann.
Okay.
Was kann ich also alles für Exceptions verwenden?
Grob gesagt, eine passende, die Java schon vorgibt, wie zum Beispiel
eine File-Not-Fault-Exception würde sich anbieten oder an vielen
Stellen macht es eben auch Sinn, eigene Exceptions zu definieren, die
dann charakterisieren, was ist denn jetzt genau in meinem Programm
hier schiefgegangen.
Dazu kann ich eben von Exception oder Runtime-Exception ableiten und
neue Unterklassen definieren.
Muss dann die beiden Standard-Konstruktoren, den leeren und den mit
einer String-Botschaft implementieren.
Kann natürlich auch noch eine komplexere Hierarchie von Exceptions
definieren, gerade in komplexeren Programmen kommt das doch oft vor.
Und genau, sollte das eben auch tun, wenn es wirklich sich um eine
Ausnahme handelt, die speziell für mein Programm ist.
Ich sollte die Exceptions, die in der Java API vorgesehen sind, eben
auch nur für die Zwecke benutzen, die in der Dokumentation dieser
Exceptions auch beschrieben sind.
Weil das natürlich sonst meinen Code irgendwie extrem unleserlich und
schwer verständlich macht, wenn ich einen Exception-Typ werfe, aber
eigentlich was ganz anderes damit meine.
Also diesen Exception-Typ, was weiß ich, ein Illegal-Argument
Exception werfe, wenn eigentlich nur der Hund Bananen zu fressen
gekriegt hat.
Also meinen Hund in meinem Modell meine ich jetzt.
Das passt dann natürlich nicht.
Genau.
Insgesamt so ein paar Hinweise zur Verwendung von Exceptions und auch
gleich Faustregeln, wie man damit umgeht.
Exceptions sollen eben dazu dienen, Ausnahmen aus dem normalen
Programmfluss herauszuziehen, also vereinfachen, indem sie eben es
ermöglichen zu trennen zwischen dem normalen erwarteten Ablauf und
irgendwelchen Ausnahmesituationen, die in gewisser Weise natürlich
auch erwartet sind, sonst hätte ich kein Exception dafür formuliert,
aber eben Ausnahmen von der Regel darstellen sollen.
Ich sollte sie dann im Javadoc-Kommentar meiner Methode auch
beschreiben, dokumentieren.
Da gibt es dann dieses Add-Throws, wohinter ich dann die Dokumentation
unter welchen Bedingungen meine Methode diese Exception werfen würde,
beschreiben kann.
Das sollte ich machen mit allen deklarierten Exceptions, die also in
der Signatur meiner Methode vorkommen, und auch denjenigen nicht
deklarierten Runtime Exceptions, wo ich schon sagen kann, dass sie mit
einer gewissen Wahrscheinlichkeit sozusagen auftreten können.
Ich sollte Exceptions nicht dafür verwenden, und das könnte ich
natürlich auch, den sozusagen normalen Kontrollfluss zu steuern, also
Beispiel dafür wäre direkt das hier unten.
Ich könnte jetzt ja mit diesem Mechanismus des Exception, des Try
-Catch auch eine Schleife einfach programmieren, mit der ich durch ein
Array hindurch schreite, eine Wild-Schleife, wo ich immer nur i++
mache, also immer nur einfach diese Zählervariable hier hochzähle, und
dann auf ein Array damit zugreife, und ich könnte jetzt sagen, ich
prüfe eigentlich gar nicht, ob das Ganze hier noch innerhalb des
Arrays ist, weil in dem Moment, wo ich außerhalb des Arrays aus dem
Array hinauslaufe, würde ja eine Exception geworfen werden, und das
benutze ich sozusagen, um meine Schleife planmäßig zu beenden.
Also das funktioniert, das könnte man so hinschreiben, quasi eine Wild
-Schleife ohne wirkliche, naja, hier hat man schon eine
Abbruchbedingung, aber ohne wirklichen Check, ob ich mit diesem Index
hier aus dem Array herauslaufe, und dann einfach als weitere
Abbruchbedingung würde ich, oder es ist in diesem Beispiel so gemacht,
dass man die Exception als normale Abbruchbedingung der Schleife
falsch verwendet.
Und das ist eben ein ganz schlechter Programmierstil, weil man es hier
quasi für den normalen Kontrollfluss, also für eine Situation, die man
ja logischerweise erwartet, wenn man dieses i hier hochzählt,
missbraucht.
Zum einen schlechter Stil, weil Exception eigentlich dem Leser das
Programm kurz sagen sollte, hier ist eine Ausnahmesituation und eben
keine normale Situation, und es ist außerdem unnötiger Aufwand, dieses
Objekt hier an der Stelle zu erzeugen, hier wieder auszulesen, also
auch einfach nicht performant das so zu machen.
Auf der linken Seite hier im Raum ist es ein bisschen unruhig, ich
glaube hier unten, oder genau.
Einer hat es auch noch nicht gemerkt.
Im roten Pulli.
Ja, genau.
Sie sind gemeint, wollte Ihr Kommilitone Ihnen mitteilen.
Danke.
Genau.
Das macht man also nicht, den normalen Kontrollfluss damit steuern.
Ein paar weitere Faustregeln, was man auch nicht tun sollte, oder was
man sogar als verboten formulieren kann, ist mit diesem Problem, dass
Exceptions deklariert werden müssen, in meinen Methoden so umzugehen,
dass ich einfach quasi in meiner Main-Methode einen großen Try-Catch
-Block baue, der alles fängt.
Und das ganze Programm quasi innerhalb dieses einen Try-Catch-Blocks
formuliere, das ist nicht sinnvoll, sondern man sollte Fehler und
Ausnahmen immer schon so früh wie möglich und sinnvoll behandeln.
Da gibt es keine Ausnahmen von dieser Regel.
Ebenso nicht sinnvoll verboten wäre ein leerer Catch-Block, also wenn
hier oben irgendwas passiert, dass eine Exception ausgelöst wird, ist
es kein guter Ziel, keine gute Reaktion, dann einfach zu sagen, ich
fange diese Exception und mache dann nichts, einfach nur um zu
vermeiden, dass ich diese Exception in meine eigene Signatur
übernehmen muss.
Das ist natürlich auch nicht im Sinne des Erfinders.
Gibt's Fragen da hier irgendwo mittig?
Oben in der drittletzten Reihe?
Wäre ein Catch-Block auch verboten?
Und was wir eben auch schon mal angedeutet hatten, es ist auch kein
guter Ziel oder verboten, einfach nur den Typ Exception zu fangen, um
irgendwie alles abzudecken, sondern Fehlerbehandlung sollte eben
feingranularer auf den tatsächlich vorgekommenen Typ der Exception
reagieren.
Genau, noch schlimmer wäre es auch zu sagen, man fängt irgendwie die
ganze Oberklasse, also die Throwables von sowohl Errors und
Exceptions, also da absolut keine Ausnahme.
Genau, ein Prinzip, was man bedenken sollte beim Arbeiten mit
Exceptions, ist es, möglichst früh Fehlererkennung einzubauen, also
das heißt, den Try-Catch-Block möglichst eng, um die Stelle, wo der
Fehler auftreten kann, zu haben.
Das heißt jetzt nicht, dass man wieder lokale Fehlerbehandlung macht,
also es muss nicht absolut eng sein, sondern einfach so eng wie
sinnvoll, sozusagen.
Was dann auch dabei hilft, wenn solche Fehler auftreten, man das
Programm debuggen muss, den Suchbereich dann möglichst klein zu haben,
am Anfang.
Man kann diese frühe Fehlererkennung, frühen Fehler, oder frühen
Abbruch dann insbesondere bei irgendwie inkonsistentem Programmzustand
machen, also es macht Sinn, nicht einfach irgendwelche Parameter immer
wieder zurückzugeben und dann könnte ich halt irgendwann mal den
Fehler bekommen, dass ich versuche, auf eine Nullreferenz zuzugreifen,
sondern dann schon an verschiedenen Stellen im Programmcode immer
wieder zu prüfen, ist denn auch was Sinnvolles in meinen Parameter
drin, wenn ja, arbeite ich weiter, wenn nein, würde ich halt schon
möglichst früh so eine Ausnahme formulieren können und nicht erst,
wenn irgendjemand versucht, auf dieses Objekt auch zuzugreifen.
Man kann sich auch merken, für eine einzelne Methode zu versuchen,
möglichst defensiv zu programmieren und davon also auszugehen, dass
die Anwender der eigenen Methode, man kann jetzt sagen, die
absichtlich missbrauchen oder zumindest Fehler machen, aber zumindest
beim Implementieren einer einzelnen Methode dann eben am Anfang
überprüfen, sind die Parameter denn auch tatsächlich innerhalb des
Wertebereichs, wo ich sie erwarte, zum Beispiel nicht negativ oder
nicht null und so dann eben Fehlerfälle direkt bemerken.
So könnte ich dann also direkt bemerken, dass irgendjemand meine
Methode falsch benutzt, dann direkt an der Stelle eine Exception
werfen, anstatt dass sich dieser Fehler relativ unbekannt noch fünf
Methoden weiter propagiert und da dann erst zu einem Fehler führen
würde.
Das heißt, ja, um sowas zu machen, so defensive Programmierung,
braucht man eben zusätzliche Abfragen im Programmcode, wo man die
Annahmen, welche Werte, welche Zustände liegen denn hier jetzt gerade
vor, eben explizit macht und im Fall, dass sie nicht vorliegen, dann
Exceptions wirft.
Ein Beispiel, ein schlechtes Beispiel für solche frühe Fehlererkennung
kann man auch in der Java API, also in den von Java vorgegebenen
Klassen finden, da gibt es zum Beispiel diese Klasse Properties, die
von Java vorgegeben ist, mit diesen beiden Methoden, dass man nämlich
einerseits irgendwelche beliebigen Eigenschaften abspeichern kann in
dieser Klasse Properties und dass man diese Klasse Properties dann
auch abspeichern kann, also zum Beispiel als Textdatei auf der
Festplatte.
Und da ist es nämlich jetzt zum Beispiel so, dass diese Put-Methode
keine weiteren Exceptions erstmal wirft.
Man kann hier beliebige Is, also Schlüssel, den wir gleich noch
eingeben und beliebige Werte drin speichern und erst in dem Moment, wo
ich das Store aufrufe, ist es bei den Properties so, dass geprüft
wird, dass die Keys und die Values auch in Strings sinnvollerweise
nicht konvertiert werden können, sondern Strings sind.
Also das heißt, ja, eigentlich ist hier die Annahme darunter, dass Key
und Value Strings sind, das wird aber noch nicht in der Put-Methode
geprüft, was eigentlich dann logisch wäre, da könnte ich ja schon
überprüfen, dass ein falscher Wert übergeben wurde, sondern erst
irgendwann später, was wie man sich vorstellen kann, die Fehlersuche
dann eben erst erschwert und ich würde dann die ClassCastException
eben von der Store-Methode bekommen, obwohl die eigentliche
Fehlerursache ja bei der falschen Methode der Put, der falschen
Verwendung der Put-Methode lag.
Genau, soviel zu den Exceptions.
Zusammenfassend, was sollten Sie mitnehmen, jetzt gelernt haben,
Exceptions kann ich auslösen mit Throw und dann entweder direkt in
meiner eigenen Methode behandeln mit einem Try-Catch-Block oder
deklarieren, dass meine ganze Methode diese Ausnahme werfen kann an
irgendeiner Stelle im Programm.
Gut, sollte ich sie dann behandeln, das Ganze möglichst lokal, also
möglichst nah dran an der Fehlerursache.
Exceptions sollten eben Ausnahmen bleiben, wie der Name ja schon sagt,
Exception -Ausnahme.
Das heißt, ich benutze die nicht, um meinen normalen Kontrollfluss zu
realisieren und ermöglichen es dann eben, Programmlogik und
Fehlerbehandlung dann auch voneinander zu trennen.
Fehlererkennung sollte ich so früh wie möglich einbauen, ich sollte
also in meiner Methode defensiv sein, erstmal prüfen, sind die
Parameter, die reinkommen, zum Beispiel innerhalb der ja, des
Wertebereichs, den ich auch annehme.
Ist vielleicht auch der Zustand meines Objekts gerade passend, um
meine Methode sinnvoll ausführen zu können, das kann ich am besten
alles explizit prüfen.
Wenn das nicht der Fall ist, eine Exception werfen und wenn es doch
der Fall ist, dann eben weiter arbeiten.
Exception werfen wäre jetzt hier die hintere Möglichkeit, damit
umzugehen.
Andere Möglichkeit wäre noch sogenannte Asserts zu benutzen, was wir
dann im Foliensatz zum Testen nochmal anschauen werden.
Okay.
Soviel zu den Exceptions.
Mach ich das jetzt schon mal wieder zu.
Wie gesagt, wir haben noch so zwei kleinere Themen heute, die mit
nicht ganz so langen Foliensätzen verbunden sind.
Ach ja, genau, wo wir hier an dieser Stelle einen Foliensatz zur
Rekursion, der auch ein bisschen aktualisiert wurde.
In dieser Vorlesungseinheit soll es um Rekursion gehen und dazu
einfach auch hier wieder der Literaturhinweis, wo in dem begleitenden
Buch Sie da denn noch mehr drüber lesen können.
Die Motivation für Rekursion ist es, dass sich an vielen Stellen beim
Programmieren in der Algorithmik ein sinnvolles Vorgehensprinzip ist,
zu sagen, ja, oder nach dem Prinzip Divide and Conquer vorzugehen,
also Teile und Herrsche oder aus dem lateinischen Divide et Impera.
Das heißt, das ist das Vorgehensprinzip, dass man, wenn man jetzt mit
einem komplexen Problem, ganz komplex ist es vielleicht nicht, aber
wie dem Problem eine Liste sortieren zu wollen, konfrontiert ist, ist
es eine Möglichkeit, so vorzugehen, dass man sagt, man zerteilt das
Problem in einfachere Teilprobleme, löst dann die einzelnen
einfacheren Teilprobleme und vereint die Teillösung wieder zu einer
Gesamtlösung.
Als Beispiel eben ein Sortieralgorithmus namens Merge Sort, also es
gibt ganz viele verschiedene Sortieralgorithmen, spannendes Thema,
aber hier schauen wir uns ein Beispiel an.
Herausforderung wäre oder Problemstellung wäre es, ein Array von
Zahlen zu sortieren, von klein nach groß und das Merge Sort geht dann
eben so vor, nach diesem Teile und Herrsche Prinzip, zu sagen, naja,
das Problem löse ich jetzt nicht auf den ersten Schlag, sondern ich
teile das auf in zwei Probleme, also in diesem Fall, ich teile die
Liste, die zu sortieren ist oder das Array auf in zwei Arrays, die ich
dann wieder sortiere.
Jetzt ist ein Teil Array hier immer noch schwierig in
Anführungszeichen, das heißt kein Problem, ich mache das einfach
nochmal.
Ich teile nochmal auf in dieses Array.
Auch das ist nicht mehr wirklich schwierig, aber kann ich noch weiter
zerteilen in ein noch einfacheres Problem, dass ich nämlich ein Array
mit nur einem Element habe, das ich sortieren möchte, dann bin ich
nämlich an dem Punkt beim Merge Sort, dass ich sage, jetzt habe ich ja
hier das Teilproblem schon gelöst, denn ein Element oder ein Array mit
einem Element ist ja schon sortiert.
Das heißt, mit meinem Teilproblem bin ich an der Stelle schon fertig
und gehe jetzt weiter vor, also hab jetzt quasi das Teilproblem
zerlegt, hab die einzelnen Teilprobleme hier trivial gelöst, dass ich
nämlich einfach nur noch eine elementige Listen habe und die sind
sortiert und mache dann weiter und vereine die Teillösung wieder zu
einer Gesamtlösung.
Das ist beim Merge Sort so, dass ich mir immer zwei Listen anschaue,
da jeweils die kleinsten Elemente dieser zwei Listen anschaue und die
in der richtigen Reihenfolge in ein neues Array hineinkopiere.
Also hier bei den alle elementigen Listen noch relativ einfach, jedes
hat nur ein Element, ich vergleiche die 5 und die 6, nehm das
kleinere, die 5, dann das größere, die 6 und hab jetzt hier also
wieder eine 2 oder eine Liste, ein Array mit zwei Elementen, was jetzt
sogar sortiert ist.
Dann mach ich weiter, nehm mir diese zwei hier her, vergleiche die
kleinere, dann das größere, nehm mir diese beiden her, diese beiden
her, das ist wie gesagt bei dem Mergen der einelementigen Listen sieht
das ein bisschen langweilig aus, was ich aber hier eben mache ist
dieser Schritt, ich vereine die Teillösung zu einer Gesamtlösung und
das funktioniert beim Merge Sort eben so, dass ich sage, ich füge zwei
schon sortierte Listen zusammen zu einer neuen sortierten Liste.
Hier jetzt in der zweiten Stufe kann man es besser erkennen, was da
passiert, jetzt würde ich also diese Liste hier sortiert mit dieser
sortierten Liste vergleichen, fange links an, vergleiche das erste
Element hier mit dem ersten Element da, stelle fest, die 1 ist
kleiner, vergleiche dann das erste Element hier mit dem jetzt ersten
Element in der zweiten Liste, die 3 ist kleiner und kann dann den
Rest, also wenn eine Liste fertig abgearbeitet ist, kann ich den Rest
der anderen Liste da einfach dranhängen und habe also jetzt hier
wieder durch dieses Zusammenführen eine größere sortierte Liste
erzeugt, der anderen Seite kann ich es genauso machen, 7 mit 2
vergleichen, 2 ist kleiner, 7 mit 4 vergleichen, 4 ist kleiner und
wenn ich eine Liste abgearbeitet habe, kann ich den Rest einfach
hinten dranhängen.
Jetzt habe ich es schon fast geschafft, ich habe nur noch 2 Listen,
die jetzt schon sortiert sind, die ich miteinander zusammenfügen muss.
Kann ich jetzt auch durchgehen, die Animation, ich glaube, ich brauche
nicht jedes Einzelsagen.
Vergleiche halt entsprechend diese Elemente miteinander und wenn ich
die eine Liste abgearbeitet habe, kann ich die andere hinten anhängen
und habe also am Ende diese Aufgabe gelöst, die Liste, die wir
ursprünglich hier oben gesehen haben, zu sortieren, in dem ich eben
erstmal Probleme aufgeteilt habe, bis das Problem trivial war, dass
ich nämlich eine elementige Listen habe, die per Definition sortiert
sind und dann eigentlich nur noch das Problem lösen musste, dass ich
jeweils 2 vorsortierte Listen zusammenführen, also mergen musste.
Genau.
Das ist dieses Prinzip Teil und Herrsche, Divide and Conquer, was ich
eben durch Rekursion häufig gut lösen kann.
Das Prinzip der Rekursion ist, dass man ein gleiches Berechnungsmuster
immer wieder mit immer einfacher werdenden oder immer kleineren
Eingabedaten durchführe, bis ich irgendwo soweit mein Problem
vereinfacht habe, dass ich so eine triviale Lösung finden kann und
dass die Lösung dann wieder zusammensetzen kann.
Das realisiere ich durch Methoden, die sich selbst, die sich direkt
oder indirekt selbst aufrufen.
Das ist eben dieser Begriff der Rekursion, die Methode, die sich
selbst wieder aufruft.
Das ist eben für dieses Divide and Conquer-Vorgehen, was wir gerade
gesehen haben, so die Standard-Implementierung.
Das heißt jetzt als Definition, was ist eine rekursive Methode?
Eine Methode, nennen wir sie F, heißt rekursiv oder sogar direkt
rekursiv, wenn im Rumpf von F Aufrufe an F, also Aufrufe an die
Methode selber wieder drin vorkommen.
Sie heißt indirekt rekursiv, wenn sie im Rumpf eine andere Methode G
aufruft, die wiederum F aufruft, also wenn nicht F sich selbst
aufruft, aber wenn trotzdem so ein Zyklus zustande kommt, dadurch,
dass F erst eine andere Methode aufruft, die wiederum F aufruft.
Und dann gibt es noch den Begriff endständig rekursiv einer Methode,
wenn nach dem rekursiven Aufruf der eigenen Methode kein weiterer Code
steht, dann ist die Methode so genannt endständig rekursiv.
Dann fehlt also dieser Problemzusammenführungsteil.
Kann man sich denn so vorstellen, dass bei jedem Aufruf der Methode
von sich selbst eine neue Instanz der jeweiligen Methode gestartet
wird, also eine neue Parameterbelegung auf den Stack gelegt wird, jede
Instanz hat dann ihre eigenen lokalen Variablen, die unteren der
vorherigen Instanzen der Methode bleiben erhalten.
Genau, jede Instanz hat ihre eigenen lokalen Variablen und auch ihre
eigenen speziellen Parameterbelegungen, die sollten sich natürlich
ändern über die verschiedenen Instanzen hinweg, sonst macht das nicht
so viel Sinn.
Schauen wir uns Beispiele dafür an.
Ein Beispiel wäre die Fakultätsfunktion, kennen Sie wahrscheinlich aus
der Schule, ist in der Kombinatorik oftmals hilfreich und berechnet
das Produkt, also in so einer N-Fakultät berechnet das Produkt der
Zahlen von 1 bis N, also 1 mal 2 mal 3 und so weiter bis mal N.
Man kann das dann auch rekursiv machen, diese Berechnung, in der man
sagt, der triviale Fall ist, ist, dass 0 Fakultät 1 ergibt, als
neutrales Element in der Multiplikation und N-Fakultät man auch bilden
kann als N mal N-1 Fakultät, also jetzt mal einfach die Zahlen
umgedreht und das kann man dann als rekursive Funktion in Java
schreiben, man sagt, man hat eine Methode Fakultät mit einem Parameter
N.
Wenn N größer 0 ist, also dieser Fall hier unten, echt größer 0, dann
berechne ich N mal ruf mich selber mit einem etwas kleinen Parameter
auf, also N mal wiederum die Fakultät von N-1.
Das heißt, hier ruft diese Methode sich selber auf, ist also eine
rekursive Methode, wenn ich aber hier in diese Methode reinkomme und N
ist nicht größer, gleich 0, also gleich 0, dann gebe ich 1 zurück.
Das ist also eine solche Rekursion, wo ich am Ende zu einem trivialen
Fall komme, nämlich, dass ich 1 zurückgebe und dann diese Teillösung
sozusagen wieder zusammensetzen kann.
Wie würde das aussehen, wenn ich das zum Beispiel mit 4 aufrufe, hier
unten jetzt nochmal gezeigt, wie der Programmcode für diese kleine,
einfache Funktion war.
Wenn ich das jetzt mit 4 aufrufe, würde ich also hier diese Zeile
auswerten, ist 4 größer 0.
Wenn ja, dann rechne 4 mal Aufruf der gleichen Funktion mit dem
Parameter 4 minus 1, sonst 1, das ist ja jetzt hier nicht der Fall.
Das heißt, das kann ich jetzt einsetzen, kann also sagen, ich habe
also diesen Ausdruck, der hier aktuell ausgewertet wird, ich rechne 4
mal Fakultät von 3, rufe also die Fakultätsmethode wieder mit dem
Parameter 3 auf.
Da kann ich jetzt auch wieder quasi den Methodenrumpf einsetzen und
sagen, 3 ist 3 größer als 0, wenn ja, dann nimm diesen Ausdruck, wenn
nein, nimm diesen Ausdruck.
3 ist größer als 0, also nehme ich diesen Ausdruck, bekomme also zu so
einem zusammengesetzten Ausdruck 4 mal 3 mal Fakultät von 2.
Das kann ich wieder auswerten.
Einsetzen.
Oder einsetzen, auswerten.
Ähm, hätte dann also hier diesen Zwischenausdruck 4 mal 3 mal 2 mal
Fakultät von 1.
Wenn ich das aufrufe, 1 ist immer noch größer als 0, das heißt, ich
rechne hier einmal Fakultät von 1 minus 1, also von Fakultät 0.
Das ist jetzt endlich mal hier unten unser trivialer Fall.
Also bisher sind wir immer durch diesen Endpfad hier durchgelaufen.
If n größer 0, das war bis jetzt immer der Fall, gebe ich eben diesen
Ausdruck hier zurück.
Jetzt kommen wir mal zu dem trivialen Fall, wenn wir hier Fakultät von
0 berechnen.
0 größer 0 wertet sich zu falsch aus, das heißt, ich laufe hier hinten
in den else-Zweig hinein, kann das jetzt also wieder auswerten und
hätte dann am Ende so ein Ausdruck stehen, 1 mal 1, das wird
zurückgegeben an die Methode, die das Ganze mal 2 rechnet.
Das wird zurückgegeben an die Methode, die es mal 3 rechnet.
Methodeninstanz.
Und das wird zurückgegeben an die Methodeninstanz, die es mal 4
rechnet.
Genau.
Hier steht dann also 6, hier steht dann 4 mal 6 und das Ergebnis wären
24.
Also hier sehen wir, hier wird die Methode, die gleiche Methode, immer
diese Methode immer wieder aufgerufen, aber mit anderen Parametern,
sodass ich also verschiedene Instanzen der Methode habe, die auch im
Stack dann übereinander liegen, sozusagen, wo ich einmal mit dem
Parameter 4 aufgerufen habe, das bleibt dann liegen, ich rufe dann mit
dem Parameter 3 auf, ich rufe mit dem Parameter 2 auf, 1 und 0, werte
dann den Aufruf mit 0, der ist eben trivial zu behandeln, der liefert
die 1 zurück, dann kann ich den Aufruf mit 1 auswerten und kann mich
dann also so wieder durch diese Verschachtelung zurückarbeiten und den
Wert 24 ausgeben.
Mhm.
Ähm, anderes Beispiel für so eine rekursive Funktion oder für eine
tatsächliche Funktion aus der, auch wieder hier, Kombinatorik, die ich
gut rekursiv in Java abbilden kann, ist die Binomialfunktion, auch das
wie gesagt aus der Kombinatorik, wenn ich sowas wie ähm, was sind die
Möglichkeiten, äh, die Anzahl der möglichen Ziehungen beim Lotto
ausrechnen möchte, dann kann ich eben die Binomialfunktion nehmen und
sagen 49 Kugeln gibt es, 6 davon wähle ich aus.
Also nur für den Hintergrund so ein bisschen.
Ist jetzt nicht genau entscheidend, dass Sie wissen, wozu man das in
der Kombinatorik braucht, sondern einfach ein schönes Beispiel einer
Funktion, die man schön rekursiv lösen kann.
Auch noch ein schöneres Beispiel, vielleicht als eben, denn wenn man
die Binomialfunktion nach so dieser Standarddefinition berechnen
würde, im eigenen Programm, ist das äh, Overflow gefährdet, im Sinne
von es kann gut sein, dass hier Zahlen erzeugt werden, Fakultäten
werden halt recht schnell recht groß, die außerhalb des Wertebereichs
meines Datentyps liegen.
Das heißt, so würde ich es vielleicht im Programm gut nicht direkt
berechnen wollen.
Ich würde dann lieber mir zunutze machen, dass ich die
Binomialfunktion auch anders ausdrücken kann, eben rekursiv ausdrücken
kann, sagen kann n über 0 ist 1, n über n ist auch 1, also es gibt nur
einen Weg, 0 Elemente aus einer n-elementigen Menge auszuwählen.
Es gibt nur einen Weg, n Elemente aus einer n-elementigen Menge
auszuwählen, nämlich alle.
Das sind dann so die Eckpunkte, sage ich mal, dieser Rekursion und
dann kann ich nämlich sagen, dass n über k diesem Ausdruck hier
entspricht.
n minus 1 über k minus 1 plus n minus 1 über k.
Ich habe jetzt keine Fakultät mehr drin vor, also keine, ja, ist der
Gefahr eines Überlaufs nicht mehr so groß genug.
Bin ich trotzdem noch da, aber nicht mehr so groß.
Mit dieser Formel kann ich jetzt eben die Binomialfunktion auch in
Java dann rekursiv berechnen.
Ich kann also sagen, aha, n über k ist 1, falls k gleich 0 oder k
gleich n ist und entspricht sonst diesem Ausdruck hier.
Diese Darstellung, sage ich mal, der Binomialfunktion kann ich dann
leicht im Programmcode überführen, so wie hier gezeigt.
Ich habe eine Funktion binom mit zwei Integer-Eingabe-Parametern.
Hier zuerst der triviale Fall geprüft, also der obere, wenn k kleiner
0 Achso, das ist erstmal die Rahmenbedingungen.
Jetzt könnte ich hier eine Exception schmeißen oder ich sage, ich gebe
0 zurück.
Also wenn k sowieso außerhalb der Parametergrenzen liegt, dass das
Ganze hier Sinn macht, gebe ich 0 zurück.
Fehlerbehandlung.
Erstmal meine Parameter prüfen.
Im Prinzip Inhalt der vorherigen Vorlesung.
Dann kommt hier die eigentliche Rekursion, die eigentliche Abarbeitung
der Binomialfunktion, nämlich erst der triviale Fall.
Wenn k gleich 0 ist oder k gleich n ist, gebe ich 1 zurück.
Das ist einfach.
Das ist das einfache Teilproblem in meiner Rekursion, auf das ich
runterbrechen wollte.
Wenn das nicht der Fall ist, gebe ich oder rufe ich mich selber wieder
auf.
Ruf die Methode sich selber wieder auf.
Einmal mit den Parametern n-1, k-1 Das entspricht diesem Term hier.
Plus nochmal dem Aufruf meiner eigenen Methode mit den Parametern n-1
und k.
Das entspricht also diesem Term hier.
Das heißt, ich löse im Sinne das Problem hier nicht direkt, sondern
ich zerteile es in zwei Teilprobleme, diesen Term und diesen Term und
rufe die eigene Methode binom.rekursiv wieder auf.
Was würde dann zum Beispiel beim Aufruf mit 3 und 2 passieren?
3 und 2 hätte ich zwei Parameter.
n ist 3, k ist 2.
Ich würde also nicht in diesen trivialen Fall hineinlaufen.
Wir haben jetzt mal diese Überprüfung, dass es innerhalb des
Intervalls liegt, hier weggelassen, sonst wäre das ein bisschen lang
geworden.
Wir gehen also nicht in den trivialen Fall hinein, sondern wir gehen
sozusagen in den rekursiven Fall hinein und rufen, zerteilen das
Problem in eben diese zwei Teilprobleme, die Binom-Funktion nochmal
mit 3-1 und 2-1 aufzurufen und nochmal mit 3-1 und 2 aufzurufen.
Das heißt, was passiert hier?
Dieser Aufruf käme ja zuerst, wird hier zuerst ausgewertet, oder
erstmal können wir einsetzen, was die Zahlen denn tatsächlich sind,
dann wird dieser linke Ausruf zuerst aufgewertet.
Ich rufe die gleiche Funktion also nochmal mit 2 und 1 auf.
Auch da komme ich nicht in den trivialen Teil hinein, sondern ich
komme in diesen Fall hinein, rufe also nochmal rekursiv diese Binom
-Funktion auf mit 2-1 und 1-1 und auf der anderen Seite mit 2-1 und 1.
Das werte ich erstmal hier aus, rufe also Binom 1-0 und 1-1 auf,
erzeuge also noch eine Instanz dieser Methode, die liegen jetzt
tatsächlich auch auf dem Stack so übereinander, wie das hier
dargestellt ist.
Wenn ich Binom mit 1 und 0 aufrufe, dann komme ich hier endlich mal in
den trivialen Fall hinein, dann kann ich nämlich sagen, aha, 1 über 0
ist 1.
Triviales Teilproblem.
Gebe also 1 zurück.
Das wird ausgeführt.
Kann jetzt also hier meine 1 einsetzen, den zweiten Teil hier
ausführen.
Auch da komme ich jetzt in den trivialen Fall hinein, weil ich hier 1
gleich 1 stehen habe, also n über k, wenn n und k gleich sind.
Auch der einfache Fall, auch da 1.
Kann also auch hier die 1 zurückliefern und kann also so mein Ergebnis
sozusagen von unten wieder zusammensetzen.
Ich habe also jetzt hier beide Teile ausgewertet und kann jetzt für
dieses für diese Instanz der Methode entsprechend 2 zurückgeben.
Setze das dann hier ein und muss dann jetzt nur noch diesen zweiten
Teil des rekursiven Aufrufs hier abarbeiten.
Da ist es jetzt schon gleich einfach.
Binom-Funktion von 2 und 2 kommt halt auch gleich in diesen trivialen
Fall rein.
2 gleich 2 heißt ich gebe 1 zurück, addiere es hier und bekomme das
Ergebnis 3.
Wie gesagt, das Schöne an dieser Rekursion, man hat immer nur das
Teilproblem zerlegt in wieder diesen Ausdruck ja, den wir hier eben
gesehen hatten.
Diesen hier.
Und habe an den Enden dieser verschiedenen Auspfade, die ich weiter
aufgemacht habe oder dieser verschiedenen Methodeninstanzen immer nur
diesen trivialen Fall gelöst, dass ich festgestellt habe, aha, die
Binom -Funktion von n über 0 oder n über n ist eben 1.
Das ist das Schöne, das Elegante im Prinzip daran.
Am Rande vielleicht so ein bisschen bemerkt, das kann ein bisschen
ineffizient sein, diese Binomialfunktion so zu implementieren, wie wir
es gerade gesehen haben, weil man da ja schon solche häufigen
Kombinationen wie vielleicht 2 über 3 oder so immer wieder ausrechnen
müsste, bisschen effizienter wäre das also, wenn man ein Cache
einführt, also wenn man sich noch einen Speicher dazu einführt, wo man
sich Parameter, die man schon mal ausgerechnet hat,
Parameterkombinationen, die man schon mal ausgerechnet hat, einfach
abspeichert und nicht nochmal neu ausrechnet.
Ja, brauchen wir jetzt glaube ich nicht durchgehen, können Sie sich
dann auch anschauen.
Also einfach die Möglichkeit hier ein bisschen Laufzeit womöglich zu
gewinnen mit einer gewissen Wahrscheinlichkeit, allerdings auf Kosten
von Speicherverbrauch, der dann höher ist, was ein häufiger Bait-Off
ist an der Stelle.
Okay, soviel auch schon zur Rekursion.
Was mache ich also bei der Rekursion?
Ich löse Probleme dadurch auf, dass ich erstmal eine kleinere Instanz
des Problems berechne, immer wieder, bis irgendwann die Instanz des
Problems so klein ist, dass die Lösung offensichtlich oder zumindest
einfach ist und setze dann diese Teillösung wieder zusammen.
Ich habe da verschiedene Instanzen meiner Methoden, die wir so
aufeinander aufbauen, die wir ja immer wieder voneinander aufgerufen
werden.
Ich habe die besondere Schwierigkeit, das hatten wir jetzt noch nicht
erwähnt, dass es dann nicht ganz so offensichtlich unbedingt immer
ist, ob das, was ich da zusammengebaut habe, diese rekursiven Aufrufe
auch irgendwann mal aufhören.
Ich muss ja schon sicherstellen, dass in allen diesen Pfaden, wo ich
immer neue Methoden mich selber wieder aufrufe, am Ende ich auch mal
zu der trivialen Lösung komme und so die Lösung dann nachher wieder
zusammensetzen kann.
Da muss ich mir also, wenn ich so eine rekursive Funktion baue, eben
zweimal Gedanken drüber machen, ist das auch wirklich sichergestellt
an der Stelle.
Und Rekursion an sich, vielleicht noch erwähnt, braucht halt oftmals
ein bisschen mehr Speicher, weil ich ja eben diese ganzen Instanzen
der Methoden im Stack speichern muss.
Und bei den Beispielen, die wir hier gesehen haben, mit so kleinen
Zahlen, wie eine Mialt-Funktion von, was hatten wir da, drei über zwei
oder so, ist natürlich noch recht wenig.
Wenn ich da jetzt große Zahlen hineinstecken würde, kann man sich
vorstellen, dass diese Aufruftiefe relativ hoch wird, dass ich also
relativ viele Methodeninstanzen im Speicher dann halten muss und sogar
linear viele zur Zahl der Aufrufe.
Wir haben ja sowieso schon Schleifen kennengelernt und Sie könnten
sich jetzt fragen, was ist denn jetzt eigentlich der Unterschied oder
wann sollte ich jetzt so Rekursion benutzen, um Dinge zu lösen und
wann sollte ich einfach eine normale Schleife benutzen, wo ich ja auch
wiederholt irgendwelche Problemteile lösen kann.
Und ein Unterschied, was man sich zuerst mal anschauen kann, ist die
Anzahl der Iterationen, die ich zur Lösung eines Problems bräuchte,
klar von vornherein oder nicht.
Wenn sie klar ist, macht es Sinn, das Problem in der Schleife zu
lösen, wie zum Beispiel für jedes Element eines Arrays irgendwas
durchzuführen, dann weiß ich im Voraus, aha, ich werde genau so viele
Schleifen Iterationen brauchen, wie das Array Elemente hat.
Wenn das unklar ist, dann wäre das ein Fall, wo ich vielleicht ganz
gut Rekursion und dieses Zerteilen in Teilprobleme, wo es dann
irgendwann klarer wird, benutzen kann.
Vorteil von Iterationen an der Stelle ist noch, dass ich die
Komplexität dann auch leichter abschätzen kann.
Klar, wenn ich schon im Voraus weiß, wie viele Iterationen brauche ich
denn hier, ich muss durch das Array nur einmal durchmarschieren, dann
habe ich halt einen guten Eindruck davon, wie komplex die
Problemlösung tatsächlich ist und ich kann auch leicht abschätzen, ob
das Ganze terminiert.
Also, wenn ich eine For-Each-Schleife habe, dann habe ich das halt
schon schön im Voraus per Definition erledigt, dass diese Schleife
auch terminiert, aber auch wenn ich eine For-Schleife mit so einer
Schrittvariablen habe, kann ich auch da besser sehen, terminiert es
oder nicht.
Wenn ich an der Schleife, an der Schrittvariable selber nicht dran rum
vorwerke, dann kann ich es eben auch leicht auf Korrektheit
überprüfen.
Das ist bei den Rekursionen eben ein bisschen unübersichtlicher, wie
man das gerade schon gesehen hat.
Weitere Nachteile von der Rekursion sind noch, dass ich eben auch
viele Methodenaufrufe wieder habe, was einen gewissen Overhead zur
Laufzeit erzeugt.
Die Java Virtual Machine muss eben diese Methodeninstanzen auf dem
Stack immer neu anlegen, muss also ein bisschen mehr im Speicher tun,
als das bei Iterationen bei einfachen Schleifen womöglich der Fall
ist.
Es ist auch speicheraufwendiger, dadurch, dass ich eben diese ganzen
Methodeninstanzen ablegen muss.
Und wie gesagt, schwieriger abzuschätzen, ob es denn jetzt terminiert
oder nicht.
Das sind dann so die Vor- und Nachteile.
Vielleicht noch ein Beispiel eines Problems, was man mit Rekursionen
schön lösen kann, was zur Jahreszeit passt.
Es gibt die Kochsche Schneeflockenkurve, ein Fraktal, wo man wie folgt
vorgeht, mit einer Geradenlänge anfängt und das Ganze dann bildet,
indem man aus dem Geradenstück das mittlere Drittel entfernt und dafür
ein Dreieck aufsetzt.
Das würde dann so aussehen, sieht noch nicht nach einer Schneeflocke
aus, aber wenn man das jetzt wieder für jede jetzt entstehende Gerade
nochmal macht, bekommt man so ein Muster.
Wenn man es dann nochmal macht, bekommt man so ein Muster und das kann
einem ganz weihnachtlich dann zumute werden schon.
Also passend für diese Jahreszeit.
Genau.
Soviel auch schon zu, was heißt auch schon, aber soviel zu der
Rekursion.
Eine kleine Vorlesungseinheit haben wir noch, aber die ist auch nicht
besonders lang, von daher passt das von der Zeit wahrscheinlich noch
einigermaßen gut.
Drittes Thema für heute nach den Exceptions, nach der Rekursion wäre
ein kleiner Überblick über die Java API, die Ihnen ein bisschen auf
den Übungszetteln schon begegnet ist.
Da stand ja manchmal, Sie dürfen die eine oder andere Klasse aus
diesem Java-Lang-Paket verwenden.
Dieses Java-Lang-Paket ist eben eines der Pakete, die von der Java
-Umgebung bereitgestellt werden, um einfacher zu programmieren, um
nicht alles neu erfinden zu müssen, um das Rad nicht dauernd neu
erfinden zu müssen.
Sind auch im Buch teilweise beschrieben.
Was bedeutet das erst mal?
Java API.
API bedeutet Application Programming Interface und ist einfach eine
Sammlung von Klassen und Paketen für häufig benötigte Funktionalität,
sodass ich das Rad eben nicht immer wieder neu erfinden muss und dies
mir erlaubt, Java Programmierung auf höherer Ebene durchzuführen.
Ich muss also nicht erst mal wieder neu mir überlegen, was ist denn
eine Linked List, sondern das kann ich aus der Java API verwenden und
kann mich direkt mit meinem eigentlichen Problem in meiner Domäne
beschäftigen.
Sie kennen einige Klassen aus der Java API ja auch schon, sowas wie
Object String, Math, Enums, Comparable, Wrapper-Klassen, also da ist
jetzt dieses Boolean großgeschrieben, Integer großgeschrieben mit
gemeint.
Genau.
Sie finden Informationen zu dieser API, wenn man sich die mal
anschaut, echt groß ist, unter dieser URL und ein bisschen
strukturiert ist das anhand von Paketen eben und drei, wenn die drei
wichtigsten Pakete sind, Java Lang, also so Sprachfeatures von Java,
diese Basisfunktionalität bereitstellen, sowas wie Object String,
Math, Enum, Java Util, ganz wichtig, weil da das Java Collections
Framework drin liegt, wo wir gleich noch ein bisschen zu sagen werden,
wo eben sowas wie Listen schon vordefiniert sind, muss man also nicht
von Hand machen.
Auch Zeit- und Datungsfunktionen muss man eigentlich nicht von Hand
machen, sind da drin.
Definiert ist natürlich ein schöner Lerneffekt, das doch mal von Hand
zu probieren, wie Sie ja festgestellt haben.
Und Java IOs sind daneben Klassen zur Ein- und Ausgabe, um zum
Beispiel aus Dateien zu lesen oder von der Konsole zu lesen.
Die Java API, was man darunter, wenn man jetzt sich mal dieses, so ein
Bild aus der Java Welt anschaut, was Java alles ausmacht, von der
Sprachebene, Compiler, was alles so die einzelnen Komponenten von Java
sind, dann ist mit Java API meistens diese Java SE API gemeint, wo
eben hier verschiedene, also hauptsächlich in diesem Bereich,
verschiedene Programmierschnittstellen zur Verfügung gestellt sind.
Genau, was ich gerade nicht gesagt habe, Application Programming
Interface heißt Programmierschnittstelle, das heißt, das sind Klassen,
die ich in meinen Quellcode einbinden kann.
Also eine Programmierschnittstelle auf Quellcode-Ebene.
Ich kann also die Klassen in meine Klasse importieren und dann
entsprechend im Quellcode einfach aufrufen.
Das findet sich eben in der ganzen Java Welt, die wir mal hier
dargestellt haben, in dieser Java SE API größtenteils wieder und hier
unten werden dann eben diese wichtigen Pakete lang und util und hier
input, output.
Wie gesagt, ein ganz wichtiges Element von der Java API ist das Java
Collections Framework.
Wir müssen uns also unsere Listen nicht selber bauen, sondern dafür
gibt es schon ganz viel vorgefertigtes Collection als einfach nur eine
Sammlung von Objekten, also was allgemeineres als ein Array, hatten
wir ja auch schon kennengelernt.
Dieses Java Collections Framework bietet mir eben eine einheitliche
Architektur zur Repräsentation und Manipulation von solchen
Collections, insbesondere dadurch, dass es eine einheitliche
Schnittstelle für solche Collections definiert und dann auch viele
sinnvolle Implementierungen, um Collections umzusetzen, auch schon zur
Verfügung stellt.
Erstmal die Schnittstelle abstrahiert von der Implementierung, aber
dadurch, dass ich die Implementierung auch bekomme, habe ich halt
einen geringeren Programmieraufwand.
Es gibt da 14 generische Interfaces von verschiedenen Sorten von
Collections.
Hier sind ein paar dargestellt und dann jeweils wieder mehrere
Implementierungen, die diese Interfaces realisieren.
Jetzt fragen Sie sich vielleicht, wieso ist Collection nicht nur ein
Interface?
Es gibt ein Collection Interface mit ein paar wenigen Methoden, die
wir gleich auch sehen und dann gibt es eben auch nochmal davon
abgeleitete Interfaces wie zum Beispiel das Interface einer Menge, was
also explizit macht oder in seinem Vertrag enthält, dass diese Menge
von Elementen nicht sortiert ist und dass da auch ein Element mehrfach
drin vorkommen kann, nicht mehrfach drin vorkommen kann in so einer
Menge, genau, nicht mehrfach drin vorkommen kann, sondern nur einmal.
Dahingegen ist eine Liste so definiert, dass es eben eine Menge oder
eine Collection ist mit einer Ordnung darauf, also die Reihenfolge der
Elemente ist Teil dieser Collection.
Die Liste mit den Elementen 1, 2 ist nicht das gleiche wie die Liste
mit den Elementen 2 und 1.
Das wäre dann also die Definition einer Liste und da gibt es
verschiedene Implementierungen für, anderes Beispiel wäre noch die
Schnittstelle einer Queue, also einer Warteschlange.
Das kann ich verwenden, wenn ich Sachen nach und nach abarbeiten
möchte, dann kann ich die in einer Queue zwischenspeichern.
Das wären alles Beispiele von unter Interfaces dieser Collection, also
einer Sammlung von Objekten.
Daneben, aber auch in dem Java Collection Frameworks gibt es auch noch
die Map, also eine Abbildung von irgendwelchen Schlüsseln, sehen wir
gleich auch nochmal auf eine Menge von Werten, aber kommt gleich
nochmal auf der Folie.
Erstmal zu dem Interface Collection selber.
Das ist eigentlich so das Beispiel in Java, was diese generischen
Interfaces benutzt, so hatten wir es ja oder mit Listen auch, die
Generics in der vorherigen Vorlesung motiviert.
Das ist bei den Collections Interfaces eben auch so, die sind immer
generisch, das heißt, sie geben an oder sie haben einen Typparameter,
welcher Typ von Inhaltselementen denn jetzt in der Liste abgespeichert
werden kann.
Collection, erstmal die allgemeine Oberklasse, einfach eine Menge,
oder Menge sollte ich jetzt nicht sagen, eine Sammlung von Objekten,
die noch keine Aussage darüber trifft, ob das geordnet ist oder ob das
Duplikate enthalten kann.
Kann ich aber schon auch in meinem Code verwenden, wenn ich eben alle
diese Aussagen noch nicht treffen möchte oder die noch nicht bekannt
sind.
Und abgeleitete Subtypen davon wäre eben die Menge, wo ich dann jedes
Element nur einmal drin haben kann.
Die Liste, wo eben die Position noch relevant ist, der einzelnen
Objekte oder eine Warteschlange.
Das Interface Collection hat dann auch schon eine Menge von Methoden,
die es selbst zur Verfügung stellt.
Einige kommen Ihnen aus der Vorlesung zu den Listen schon bekannt vor.
Ich kann prüfen, ob ein Element vorhanden ist.
Ich kann prüfen zum Beispiel, ob eine Collection leer ist, wie die
Größe ist.
Ich kann ein Element hinzufügen oder entfernen, kann mir einen
Iterator geben lassen, prüfen, ob alle Elemente vorhanden sind,
contains all, kann eine Menge von Elementen oder genauer gesagt eine
Collection von Elementen, die vom Typ her passt, das ist dieser
Ausdruck hier, hinzufügen, oder ich kann auch eine Menge oder eine
Collection von Elementen aus der Liste löschen lassen.
Eine Liste mit, oder eine Collection mit Clear ganz leeren, leer
machen, alle Elemente hinausgeben und ich kann mir immer diese
Konvertierung in Arrays mit diesen beiden Methoden ermöglichen.
Und eine etwas neuere Methode, ich kann auch sagen, ich rufe meine
Collection auf mit einer zweiten Collection und behalte nur all die
Elemente in meiner Collection, die auch in dieser übergebenen
Collection sind und alle anderen werden gelöscht.
Also so ein umgedrehtes Remove All an der Stelle.
Das sind nicht alle Methoden, die die Collections haben, aber so die
wichtigsten oder die nicht so speziellen.
Genau, das ist das Interface von Collection, was Sie jetzt hier noch
nicht kennen, wenn wir diese Signaturen uns hier anschauen, ist dieses
Fragezeichen.
Wir haben ja bis jetzt über die Generics gesprochen, der Typ Parameter
hier, eine Liste hat also einen bestimmten Typ von Elementen, der da
drin liegen darf.
Wenn ich jetzt aber Methoden angeben möchte, die auf dieser Liste
arbeiten, dann kann ich hier noch ein bisschen flexibler zulassen,
welche Arten von Collections denn jetzt hier als Parameter dieser
Methoden übergeben werden können.
Dieses Fragezeichen, erstmal dazu, nennt man Wildcard.
Wildcard auf Englisch, ein Kartenspiel, sowas wie ein Joker, allgemein
einfach Platzhalter, kann man sich das übersetzen.
Genau.
Und die kann ich gerade bei sowas wie Collections verwenden, um ein
bisschen flexibler zu sein, welcher Typ mit welchen Typ Parametern
Methoden zum Beispiel auch umgehen können.
Ich kann also in generischen Typen als Typ Parameter auch diese
Wildcard verwenden, also Collection?C zum Beispiel schreiben und damit
meine ich, dass hier ein beliebiger Typ von Collection kommen kann,
beliebig aber fester Typ.
Und wenn ich hier das Fragezeichen formuliert habe, darf ich keine
Annahmen darüber machen, was das denn jetzt für ein Typ ist, sondern
ich muss einfach damit arbeiten, dass das unbekannt ist, sozusagen.
Es gibt da drei Arten von Wildcards, zum einen einfach nur das
Fragezeichen, unbound, also ich weiß nichts über diesen Typ, der da
später kommen kann.
Ich kann aber auch nach unten eingeschränkte Wildcard haben, also eine
Lower Bound Wildcard, dass ich sage, hier weiß ich zwar nicht genau,
was für ein Typ, es ist aber ein fester und es ist auch eine
Oberklasse von einem anderen fest anzugebenen Typ T.
Das wäre ein Lower Bounded Wildcard oder eben auch ein Upper Bounded
Wildcard, dass ich sage, es ist ein beliebiger Typ, aber was ich über
ihn weiß, ist, dass er eine Spezialisierung von T ist.
Wenn wir das jetzt nochmal anhand unseres Beispiels mit den
Säugetieren und Tieren und so weiter uns anschauen, würde irgendwas,
Wildcard, also heißen irgendeine dieser Klassen aus meiner Hierarchie
oder beziehungsweise alle Klassen, die ich überhaupt in meinem
Programm und in der Java API habe natürlich, könnte betroffen sein,
wenn ich jetzt sage, Wildcard Super Animal, dann können es nur diese
drei Klassen sein, die hier reinkommen als Typ.
Oder wenn ich sage, Wildcard Extents Animal, kann es eben nur die
Klasse Animal oder Spezieller sein.
Also so kann ich eben diese Wildcard für eine bestimmte
Vererbungshierarchie nach oben oder nach unten auch einschränken.
Genau, das mache ich eben, um hatte ich es hier eigentlich schon
gesagt, ich glaube schon, um flexibler sein zu können.
Das Problem dahinter wäre, hier vielleicht doch nochmal zwei Folien
zurück, um das zu erläutern, wenn ich jetzt hier nur schreiben würde,
zum Beispiel bei der Methode ContainsAll, als Parameter nur zulassen
würde eine Collection von E, also von meinem eigenen Typ Parameter E,
würde das bei den Collections bedeuten, dass ich hier nicht mit
Unterklassen diese ContainsAll Methode aufrufen kann.
Also ich könnte ContainsAll für eine Collection von Numbers dann nicht
aufrufen mit einer Collection von Integers, weil das für Java diese
beiden Klassen nicht in Beziehung zueinander stehen.
Collection von Numbers und Collection von Integers.
Hintergrund ist die Invarianz von Generics in Java, also Covariance,
Invarianz sind erstmal zwei Begriffe, um zu beschreiben, wie
Vererbungsrichtungen sich verhalten, also wie die Vererbung
typischerweise von Eingabe Parametern oder von Rückgabeparametern
richtig verwendet werden können.
In Java ist es so, dass Arrays Covariant sind, das bedeutet, wenn ich
zwei Klassen habe, die eine ist Unterklasse der anderen, Integer ist
zum Beispiel Unterklasse der von Number, dann ist ein Array von
Integer auch eine Unterklasse von Array von Number und ich kann an den
Stellen eben auch gegenseitig Variablen zuweisen.
Ich kann sagen, ich erzeuge ein neues Integer Array und verweise das
einer Variable vom Typ Number Array zu und könnte dann trotzdem hier
ein neues Integer Objekt zum Beispiel in das Array hinzufügen.
Das nennt sich Covarianz.
Kommt mit Problemen, denn hier kann ich jetzt anhand dieser Variable
nicht mehr erkennen ach, wo war das?
Das Beispiel ist schlecht.
Das ist ein neues Beispiel, was ich heute Vormittag eingebaut habe und
ich sehe jetzt, dass es schlecht ist, aber macht nichts.
Hier kann ich jetzt nicht mehr erkennen, dass ich hier zum Beispiel,
String ist ein schlechtes Beispiel, aber einen Double-Wert nicht
einfügen könnte.
Also ich könnte jetzt hier schreiben A0 gleich New Double, 4,2 zum
Beispiel.
Das würde dann erst mal vom Compiler her okay sein, aber es würde dann
zu einem Laufzeitfehler führen, weil ich natürlich in einem Integer
Array, was ja da eigentlich hinterliegt, keine Doubles speichern kann.
Muss ich noch korrigieren.
Das geht bei Generics aber nicht.
Also hier hätte ich ja eine gewisse Flexibilität, dass ich den
Unterklassen, dass ich also annehme oder weiß, dass Integers irgendwie
was Spezielleres sind als Number Arrays, wobei man sieht auch hier
gleich die Einschränkung, dass das nicht immer der Fall ist.
Generics in Java sind aber invariant.
Also das heißt, ich kann, hab keine Ableitungsbeziehung zwischen einer
Arraylist von Numbers und einer Arraylist von Integers.
Es ist zwar so, dass Integer eine Unterklasse ist von Number, aber das
überträgt sich sozusagen nicht auf die entsprechenden generischen
Collection Klassen.
Also eine Arraylist von Number und Arraylist von Integer sind von der
Vererbungsbeziehung her nicht miteinander verwandt, kann man mal so
formulieren.
Das heißt, wenn ich sowas schreiben würde wie hier, neue Arraylist vom
Typ Integer, soll zugewiesen werden einer Variable Arraylist vom Typ
Number, dann würde ich einen Komplettierfehler bekommen, weil das, wie
gesagt, einfach zwei Typen sind, die, wie gesagt, nicht miteinander
verwandt sind.
Dafür kann ich jetzt diese Wildcards benutzen, diese Bounded
Wildcards.
Ich könnte nämlich diese Vererbungsbeziehung dann doch auch für die
Generics ausnutzen, indem ich zum Beispiel hinschreiben darf, erzeuge
eine neue Arraylist mit dem Typ Parameter Integer und sage, das ist
jetzt hier was Allgemeineres, das ist nämlich eine Liste von
irgendeinem Typ, der den Typ Number spezialisiert oder vom Typ Number
erbt.
Also hier dann doch die Möglichkeit, ein bisschen flexibler
vorzugehen.
In so eine Liste kann ich dann nichts mehr hinzufügen, wenn ich so
eine Liste mit Bounded Wildcards habe.
Das würde an diesen beiden Stellen Kompilierfehler geben, weil man
hier eben nicht mehr weiß, was ist denn das jetzt für eine Liste.
Man weiß zwar, es ist eine Liste von einem Untertyp von Number, aber
ich kann jetzt nichts mehr reinstecken, weil ich nicht mehr weiß, sind
es denn jetzt Integer oder sind es denn hier Doubles.
Das heißt, da würde der Compiler direkt schon abfangen, dass das nicht
geht.
Was ich aber wieder machen könnte, ist zu sagen, ich definiere eine
neue Arraylist mit dem Typ Parameter Number und füge da alle Elemente
von B hinein.
Lesen kann ich sozusagen aus dieser Liste, weil ich ja weiß, dass hier
nur Unterklassen von Number drinstecken können.
Genau.
Bisschen kompliziert vielleicht, aber eben einfach eine Möglichkeit,
nicht hier stecken zu bleiben, dass man sagt, es gibt gar keine
Beziehung zwischen diesen beiden Arten von Listen, sondern schon so
weit es möglich ist, diese ineinander überführen zu können.
Man kann sich's vorstellen, diese Upper Bounded Wildcards als Read
-Only -Typen.
Das heißt, man weiß schon spezieller, hier bei so einer Verwendung
eines Wildcards hier steckt ein spezieller Typ drin, anders als jetzt
List von Object, da könnte man was Beliebiges drinstecken, Strings und
Doubles und Integers und hier weiß ich eben, es gibt einen festen Typ,
dessen Elemente da drinstecken können, ich weiß eben nur nicht,
welches.
Genau, das lasse ich dann vielleicht schon mal.
Und das kommt eben in diesen Collection Interfaces an verschiedenen
Stellen vor, um dann trotzdem flexibel sein zu können und zum Beispiel
in der Collection von Numbers auch ein Add-All mit einer Collection
von Integer aufrufen zu können.
So, jetzt sind wir eigentlich auch schon am Ende der Vorlesung von der
Zeit her.
Es sind jetzt auch gar nicht mehr so viele Folien.
Ich überlege gerade, ist noch irgendwas, wo ich ja, wo vielleicht der
Erik nächstes Jahr doch mal was zu sagen sollte, was ich jetzt noch
gehofft hätte, noch ansprechen zu können, wäre eben, dass dieses
andere Inhalt von den Collections, nämlich die Abbildung, die Maps,
gut, okay, aber ich will sie jetzt auch nicht länger aufhalten als
nötig, das heißt, und Java.io haben wir auch noch, das heißt, das
kommt dann beim nächsten Mal, aber was wir jetzt erstmal gesehen
haben, sind schon mal die Teile aus der Java API, der
Programmierschnittstelle, eigentlich der wichtigste Teil, nämlich
diese Java Collections und ein bisschen Tricks und Spezialitäten, die
dann mit der Verwendung von Generics noch dazu kommen.
Okay, dann wünsche ich Ihnen allen schöne Weihnachts- oder
Winterferien, wie auch immer Sie die ausgestalten möchten und genau,
wir sehen uns dann so schnell nicht wieder, heißt das, aber trotzdem
dann schon mal alles Gute für die Abschlussaufgaben und die weiteren Übungen.