 Ja, herzlich willkommen zurück zur Veranstaltung Programmierparadigmen. Wir sind hier im dritten von fünf Teilen zum Thema Control Abstraction und in diesem Teil soll es um Exception-Händling gehen, also der Frage, wie in Programmiersprachen Ausnahmebedingungen erklärt werden können und wie man die dann behandelt. Bevor wir mit dem eigentlichen Inhalt anfangen, habe ich eine kleine Aufwärmübung, wo wir uns Exceptions in einer Programmiersprache, nämlich Java, schon mal ein bisschen anschauen und die Frage für Sie ist, was macht denn dieser Code eigentlich? Also was gibt der Schluss endlich aus? Ich würde Sie bitten, das Video dann hier anzuhalten, ein bisschen darüber nachzudenken und anschließend Emilias abzustimmen, was sie denken, was dieser Code ausgibt und dann anschließend erst die Lösung anzuschauen. So, schauen wir uns mal an, was der Code hier jetzt macht. Also das Ergebnis, was schlussendlich ausgegeben wird, ist einmal eine Zeile, die er ausgibt, finally hier, also sozusagen das, was hier unten steht und anschließend dann eine Exception bzw. das, was die schlussendlich auf der Konsole ausgibt. Das steht also irgendwo Exception in und so weiter und dann kommt der ganze Stacktrace. Warum ist das so? Also zunächst erst mal hier oben wird eine Exception erstellt, nämlich weil wir einen Wert, der eigentlich Null ist, versuchen zu de-referenzieren und da versuchen, eine Funktion drauf aufzurufen, was natürlich nicht geht und daraus ergibt sich eine Null-Point-Exception. Das Ganze passiert in dem Try-Catch-Plock, das heißt, wir versuchen, bestimmte Exceptions abzufangen, allerdings nicht die Exceptions vom richtigen Typ, denn hier das erste, was wir versuchen zu fangen, ist eine Illegal-State-Exception, aber eine Null-Point-Exception ist was anderes, deswegen wird die hier nicht aufgefangen. Der zweite Catch-Plock funktioniert, denn hier wird die Null-Point-Exception tatsächlich aufgefangen und dann in eine andere Exception wieder verpackt und die wird dann nochmal geworfen, nämlich eine Runtime-Exception. So, dann haben wir um diesen Try-Catch-Plock herum einen weiteren Try-Catch-Plock, der hier unten sein Catch hat und in dem versuchen wir jetzt, eine Null-Point-Exception aufzufangen. Das Problem ist aber, dass die Exception, die jetzt geworfen wurde, ja gar keine Null-Point-Exception mehr ist, sondern eine Runtime-Exception, die eine Null-Point-Exception rappt, aber schlussendlich doch eine Runtime-Exception. Das heißt, dieser Catch-Plock hier unten wird nicht getriggert, er fängt nichts, aber was ausgeführt wird, ist dieser Finaly-Plock, denn die Java-Sprache spezifiziert, dass der Finaly-Plock immer ausgeführt wird, egal ob eine Exception geworfen wird oder nicht. Das heißt, der Code gibt Finaly hier aus und weil die Exception ja noch nicht gefangen wurde, sondern diese Runtime-Exception jetzt immer noch unterwegs ist, kommt neben, nach dieser Ausgabe Finaly hier, dann eben auch die Exception, die dann nochmal komplett ausgegeben wird und das Programm an der Stelle dann auch zum Abbruch bringt. Nach diesem kleinen Beispiel schauen wir uns erst mal an, was Exceptions dann überhaupt sind. Also ich habe das jetzt alles schon ein bisschen angenommen in dem Beispiel, weil die meisten das wahrscheinlich schon mal gesehen haben, möchte es hier aber jetzt trotzdem nochmal ein bisschen genauer definieren. Also so eine Exception beschreibt einfach nur eine ungewöhnliche Bedingung, die während der Ausführung eines Programmes auftreten kann und die nicht einfach in dem lokalen Kontext, in dem wir uns befinden, behandelt werden kann. Also wenn ich die lokal behandeln könnte, würde ich vielleicht einfach in if hinschreiben und dann für diese Bedingung irgendwas machen. Die Idee meiner Exception ist aber, dass das nicht so einfach lokal behandelt werden kann, sondern ich den standardmäßig ausführenden Control Flow verändern möchte, falls diese Ausnahme Bedingung auftritt und dann etwas anderes ausführen möchte. Und dieses andere Ding, was ich ausfühlen möchte, das ist ein sogenannte Exception-Händler und das ist bestimmter Code, der genau dann ausgeführt wird, wenn eine Exception oder vielleicht auch eine Exception einer bestimmten Art oder eines bestimmten Types, wenn diese Exception auftritt. Wann treten denn Exceptions überhaupt auf? Da gibt es im Prinzip zwei Varianten. Das eine ist, dass Exceptions implizit geworfen werden von der Implementierung der Sprache. Also in vielen Sprachen gibt es bestimmte Bedingungen, die eigentlich nicht auftreten sollten und wenn sie dann auftreten, dann wird von der Sprache Implementierung, also zum Beispiel in Java, von der WM eine Exception geworfen. Das passiert zum Beispiel, wenn ich eine Zahl versuche durch null zu teilen oder wenn ich versuche den null Wert zu de-referenzieren oder auch in einer ganzen anderen Menge von Bedingungen und in all denen wird dann implizit von der Sprache Implementierung eine entsprechende Exception geworfen, die eben diesen Laufzeitfehler angibt. Die zweite Variante ist, dass die Exception explizit vom Programm geworfen wird. Also der Programmierer an irgendeiner Stelle entschieden hat, wenn dieses oder jenes auftritt, dann möchte ich, dass durch eine Exception angeben, sodass es dann entsprechend behandelt werden kann. Das macht man, wenn irgendein Zustand erreicht ist, der illegales oder unerwartet ist, also der eigentlich nicht erreicht werden sollte. Zum Beispiel, wenn ich irgendwelche Flex habe in meinem Programm und bestimmte Kombinationen von diesen Flex eigentlich nie auftreten sollten, außer ich habe vielleicht ein Buck in meinem Programm und in dem Fall würde dann eine Exception geworfen, sodass man sieht, oh, da ist irgendwas passiert, was jetzt nicht passieren sollte und das dann entsprechend behandelt werden kann. Was man nicht machen sollte, ist, Exceptions zu verwenden, um normalen Control Flow zu beschreiben. Also man könnte im Prinzip ja statt if-else auch Exceptions verwenden und sozusagen an bestimmte Stellen springen, weil dann da der Exception-Händler ist. Das ist aber nicht sehr empfehlenswert, weil es zu sehr schwer lesbaren und wachbaren Code führt und auch einfach nicht die Idee von Exceptions ist, sondern die sollen wirklich nur für Ausnahmebedingungen verwendet werden, die eigentlich nicht auftreten sollten. Jetzt gibt es natürlich nicht nur Sprachen, die Exceptions haben, sondern es gibt auch Sprachen, in denen es diese Idee von Exceptions vielleicht gar nicht gibt oder wo ich die vielleicht gar nicht immer so verwenden kann und es auch mal interessant zu sehen, was es dann dann auch für Alternativen gibt. Es gibt im Prinzip drei andere Optionen, die man verwenden kann, um sowas ähnliches wie Exceptions zu bekommen bzw. mit so außergewöhnlichen Situationen, in denen man normalerweise eine Exception werfen würde, umzugehen. Option eins ist, dass ich einen Rückgabewert oder einen Verhalten, sozusagen definiere oder mir ausdenke, dass einfach passiert, wenn so eine Ausnahmebedingungen auftritt. Also wenn ich zum Beispiel ein Stück Code habe, was von einer Datei etwas liest und das, was gelesen wurde, dann vielleicht als String zurückgibt, dann kann es ja vielleicht passieren, dass diese Datei gar nicht existiert oder nicht richtig gelesen werden kann. Und eine Variante damit umzugehen, wenn man jetzt keine Exception hat oder vielleicht auch nicht werfen möchte, ist ein mehr oder weniger legitim Rückgabewert für diese Funktion sich auszudenken und zum Beispiel könnte das hier ein leerer String sein, der dann zurückgibt, dass in der Datei nur ein leerer String war, obwohl ich eigentlich die Datei gar nicht lesen konnte. Eine zweite Variante, das wird zum Beispiel in C sehr viel verwendet, ist, dass ich den Status dieser einer aufrufenden Funktion mithilfe eines Rückgabewertes angebe. Also ich könnte zum Beispiel einen Integer als Rückgabewert haben, der dann eventuell einen Errorcode angibt. Also bestimmte Integer-Werte geben an, dass irgendwas schief gelaufen ist, wofür ich in Sprachen wo es Exceptions gibt, vielleicht eine Exception werfen würde. Aber wenn es die eben nicht gibt, kann ich das mithilfe von so einem Errorcode codieren und der Aufrufer muss dann aber natürlich immer die Rückgabewerte überprüfen und schauen, ob der Errorcode jetzt einer dieser unerwarteten Werte ist. Die dritte Variante, das wird zum Beispiel in JavaScript häufig verwendet, ist, dass der aufrufende Code, wenn eine Funktion aufgerufen wird, noch ein Callback, also eine Funktion oder eine Closure übergibt, die dann eine, die beschreibt, was passiert, wenn etwas schief läuft. Also ich übergebe sozusagen den Error-Händler nicht über Exceptions, sondern indem ich da eine Funktion übergebe. Und ja, eine Umgebung, wo das häufig verwendet wird, ist in JavaScript oder spezifisch in Node.js, wo es diese Error-First-Konvention gibt, die im Prinzip bedeutet, dass viele Funktionen als allererstes Argument eine andere Funktion erwarten und diese andere Funktion, dieser Callback wird nur aufgerufen, wenn die Funktion irgendeine Ausnahmabedingung erreicht und sozusagen diesen Fehler angeben möchte. Wie sehen diese Exceptions jetzt aus, wenn wir sie denn haben in unsere Programmiersprache? Die bei weitem häufigste Sündhacks in modernen Programmiersprachen ist die von Try-Catch-Blocks, also das, was wir in der Aufwärmübung am Anfang ja auch schon gesehen haben. Die Idee hier ist, dass der Exception-Händler lexikalisch wirklich zu dem Block von Code gebunden wird, indem eine Exception vielleicht aufdrehten könnte und indem wir diese Exception dann auch behandeln möchten. Hier unten ist mal ein Beispiel in C++, wo wir so einen Try-Catch-Block haben. Also das gibt es diese zwei Keywords, Try und Catch, die jeweils ein Block-Code nach sich ziehen dann und was da passiert ist, dass wenn in dem Try-Block eine Exception geworfen wird, dann wird die von dem Catch-Block hier unten behandeln. Also wenn ich jetzt zum Beispiel hier unter einer bestimmten Bedingung so ein Exception namens MyError ausgeben würde, dann würde ich die hier unten behandelt und das, was hier in dem Punkt, Punkt, Punkt steht, wird dann nicht mehr ausgeführt, sondern der Kontrollfluss würde eben speziell dann gleich hier hinspringen. Was wichtig ist, ist, dass diese Catch-Blocks oft nur bestimmte Typen von Exceptions behandeln, also in dem Fall zum Beispiel MyError. Und wenn eben genau dieser Typ an Exception dann geworfen wird, wird er da behandelt. Falls nicht, wird das Ganze aber weiter propagiert an den umgebenden Code und vielleicht auch an die aufrufende Funktion. Diese Try-Catch-Blocks kann man natürlich nicht nur einfach haben, sondern man kann die auch ineinander verschachteln. Das hat mir auch schon in der Aufwärmenübung gesehen. Und was hier üblicherweise passiert, also die Semantik dieser verschachtelten Try-Catch-Blocks, ist Folgendes, nämlich dass der inneste Händler, der den richtigen Typ hat, zunächst gefragt wird und anschließend geht es dann weiter nach außen, bis ich einen Catch-Block finde, der hoffentlich diese Art Exception, die irgendwo geworfen wurde, dann behandeln kann. Also wenn ich hier zum Beispiel so ein Stück Code habe, das eine Exception werfen könnte, dann wird zunächst, falls so ein Exception auftritt, hier bei dem inneren Catch-Block geschaut, ob dieser Catch-Block die richtige Art von Exception behandelt. Also wenn hier eine Exception von Typ Sam Other Error geworfen wird, dann wird er eben hier in diesem Catch-Block entsprechend behandelt. Falls das nicht der Fall ist, also falls eine andere Art von Exception geworfen wird, zum Beispiel My Error, dann wird das in dem umgebenden Catch-Block behandelt und zwar dann in dem Exception-Händer, den wir hier unten sehen. Wenn es diese verschiedenen Typen von Exceptions gibt, das eine legitime Frage natürlich, wie man ausdrücken kann, dass verschiedene Arten von Exceptions behandelt werden soll und das ist in vielen Sprachen möglich, indem ich eine Liste von Exception-Händlers angebe, die dann eine nach der anderen gefragt werden und geschaut wird, ob das der richtige Typ ist oder das nächste der richtige Typ ist und so weiter. Das heißt, ich kann also quasi an ein Tri-Block mehrere Catch-Blocks für verschiedene Arten von Exceptions anhängen, so wie man das hier sieht. Das heißt, hier würde dann, falls eine End-of-File Exception geworfen wird, das Ganze hier behandelt. Falls eine IO Error Exception behandelt wird, das Ganze hier geworfen wird, das Ganze hier behandelt. Und dann haben wir schlussendlich hier noch dieses Punkt, was in C++ die Sündungszeit für so ein Catch-All ist. Das heißt, wenn irgendeine andere Art von Exception geworfen wird, dann wird die dann hier unten behandelt in diesem Exception-Händler. Wenn wir also einen Tri-Catch-Block haben, dann werden Exceptions da behandelt, sofern denn der richtige Typ an Exception geworfen wird. Die Frage ist jetzt, was passiert denn eigentlich, wenn es keinen passenden Händler gibt, der in der aktuellen Subroutine die Exception behandelt. Was hier passiert ist Folgendes. Das eine ist, dass wir sofort von der aktuellen Subroutine zurückkehren. Das heißt, der Kot wird nicht weiter ausgeführt, bis sie zum Return Statement kommen oder das Ende der Subroutine erreichen, sondern er kehrt sofort zurück und wird an der Stelle, wo die Funktion aufgerufen wurde, also die Call-Site, diese Exception auch wieder erstellen. Also die wird dahin propagiert. Falls es da dann einen passenden Händler gibt, der zum Beispiel den Funktionsaufruf mit Tri-Catch gerappt hat, dann wird die Exception entsprechend da behandelt. Ansonsten wird diese Exception weiter propagiert, entsprechend dem aktuellen Call-Stack. Und im Extremfall geht das bis zur Main-Funktion zurück, also bis zu dem Punkt, wo das Programm Schluss endlich begonnen hat. Und zwar all das immer genau wie bei der Funktion, wo die Exception am Anfang aufgetreten ist, nämlich so, dass wir sofort zurückkehren, also den Kot, der hinter der Kot-Location, wo die Exception geworfen wird, noch kommt. Der wird nicht ausgeführt, sondern ohne diese Subroutine zu beenden, geht es immer weiter hoch im Call-Stack, bis irgendwer diese Exception behandelt. Falls das niemand tun sollte, also wenn die Exception einfach nirgends gefangen wird, was öfters mal passiert, dann wird das Programm an der Stelle sofort unterbrochen oder abgebrochen und gibt in der Regel in irgendeiner Form aus, dass eine Exception aufgetreten ist. Schauen wir das Ganze noch mal an einem konkreten Beispiel an, und zwar in Java. Also was wir hier haben ist eine Main-Funktion, in der wir erstmal ein Objekt der klasse Neste-Tendler erstellen und dann dessen Fum-Funktion aufrufen. Und was Fum machen soll, ist uns einen String zurückgeben, den wir dann hier unten ausgeben wollen. So, schauen wir mal in Fum rein. Fum erstellt diesen String, der anfangs ein leeres String ist, und ruft dann bar auf. Und was bar machen soll, ist uns den eigentlichen Wert des Strings dann zurückgeben, den wir dann hier an die Main-Methode zurückgeben. Und in bar wird dieses String abc zurückgegeben. Was Bar jetzt außerdem macht, ist zu schauen, ob der integer-Wert, der an bar übergeben wird positiv oder zumindest 0 ist. Und falls das nicht der Fall ist, wirft bar eine illegal argument exception. Und das ist leider auch das, was hier passiert, denn wir übergeben minus 5 an bar. Das heißt, an dieser Kotstelle hier wird eine Exception erstellt, und die Frage ist jetzt, was passiert und wird hier irgendwo behandelt. Wir haben den Aufruf von bar hier oben, ja mit StryCatch, um, ja, gerappt. Das heißt, hier wird diese Exception potenziell gefangen. Allerdings wird hier nur nach einer Nullpoint der Exception geschaut, und die wird, die tritt aber hier nicht auf, sondern es ist eben eine illegal argument exception. Das heißt, die Exception wird hier nicht behandelt, sondern wird dann weiter propagiert zu dem Aufruf der Fumfunktion, und der war hier unten. Das heißt, wir schauen dann hier, ob es hier vielleicht ein Exception-Händler für eine illegal argument exception gibt, was aber leider auch nicht der Fall ist. Das heißt, an der Stelle wird von der Main-Methode dann direkt zurück, ja, die wird abgebrochen, und das Programm stürzt ab, und gibt uns einfach nur diese Exception aus, und wir erreichen nie diesen Kot, der hier unten den String S ausgibt. Schauen wir uns mal an, ob das jetzt auch alles stimmt, was ich da erzählt habe. Also wenn wir das Ganze kompilieren, das ist erstmal alles gut, und wenn wir es dann ausführen, dann bekommen wir tatsächlich diese illegal argument exception, die eben hier in Nested Handlers Zeile 15 als erstes aufgetreten ist. Also hier, wo wir die Exception werfen, dann in Zeile 6 war da aber nicht behandelt worden, und deswegen schlussendlich hier in Zeile 20 angekommen ist, und damit, weil sie eben nicht behandelt wurde, das Programm abgestürzt ist. So, eine andere interessante Frage ist, was kann man dann überhaupt alles als Exception verwenden in so einer Sprache? Also wenn ich eine Exception werfen will, was kann ich da überhaupt werfen? Das hängt ein bisschen von der Programmiersprache ab. Es gibt in manchen Sprachen bestimmte Klassen, und alle Subklassen von diesen Klassen sind Exceptions. Also in Java zum Beispiel gibt es diese eine Klasse Exception, die dann wieder Unterklassen hat, zum Beispiel Run Time Exception, und jeder Subtyp dieser Exception Klasse ist eine Exception und kann entsprechend dann mit Throw geworfen werden. Eine andere Variante, die zum Beispiel in Modular 3 auftritt, ist, dass es eine spezielle Art von Objekt gibt, so ähnlich wie Konstanten oder Typen oder Variablen, die als Exception verwendet werden können, und die ich dann auch mit so einem bestimmten Keyword, nämlich Exception, deklariere. Also dieses MTQ wäre sozusagen eine Art von Exception hier, und die könnte ich dann entsprechend werfen. Und dann gibt es auch noch Sprachen, wie zum Beispiel JavaScript, wo jeder Wert als Exception geworfen werden kann. Also ich kann zum Beispiel Throw 42 hinschreiben, was dann die Zahl 42 als Exception wirft, und dieser Wert dann schlussendlich auch ankommt. Ein bisschen sinnvoller ist es, vielleicht da zum Beispiel einen String zu nehmen, statt einer Zahl, der dann ausdrückt, warum eigentlich die Exception geworfen wird und was hier eigentlich schief gelaufen ist. Wenn jetzt eine Exception geworfen wird, ist die Frage, was macht man dann? Also wie sollte sich das Programm verhalten, wenn eine Exception auftritt? Gibt es im Prinzip drei häufig genutzte Antworten. Das eine ist, dass ich Versuche, aus dem illegalen Zustand oder aus dem fehlerhaften Zustand rauszukommen und dann die reguläre Ausführung fortsetzen. Also wenn ich zum Beispiel eine Exception bekomme, die mir sagt, dass ich nicht mehr genug Speicher habe, also dass das Programm Out of Memory ist, dann wäre eine Möglichkeit, diesen Zustand zu behandeln, mehr Speicher zu alloziieren und anschließend ganz normal mit der Ausführung fortzufahren. Eine zweite Variante, die das Ganze sozusagen lokal behandelt, aber nicht global löst, ist, dass ich meinen lokalen Zustand noch aufräume und anschließend die Exception wieder werfe, sodass sie dann woanders vollständig behandelt werden kann. Lokaler Zustand aufräumen heißt zum Beispiel, dass bestimmte Ressourcen, die alloziiert wurden, wieder freigegeben werden. Also wenn ich zum Beispiel Dateien offen habe, dann ist es ja empfehlenswert, diese Dateien noch zuzumachen, selbst wenn eine Exception geworfen wird, weil jeder Prozess ja nur eine bestimmte Anzahl von Dateien öffnen kann und ich sonst unter Umständen an die Stelle komme, dass mir das Betriebssystem einfach nicht erlaubt, weitere Dateien zu öffnen. Die dritte Variante, das ist sozusagen das, was man immer noch machen könnte, auch wenn man sonst nicht weiß, wie die Exception behandelt werden soll, ist, dass man eine Fehlermeldung ausgibt und anschließend dann das Programm terminiert. Und das ist natürlich auch das, was in vielen Sprachen standardmäßig passiert, wenn man keinen expliziten Exception-Händler angibt. Was sehr wichtig ist, ist, dass Exceptions nicht einfach nur verschlungen und versteckt werden, denn wenn ich sozusagen einen Exception-Händler definiere, in dem aber nichts mache, dann bekomme ich gar nicht mit, dass ich eine Exception hatte und das Programm wird dann weiter ausgeführt, obwohl ich eigentlich an einem Ausnahmezustand war, der in irgendeiner Form natürlich behandelt werden muss, weil ansonsten die weitere Ausführung unter Umständen auch gar nicht das macht, was sie eigentlich soll. Schauen wir das Ganze nochmal an einem konkreten Beispiel an, und zwar wieder in Java. Also was wir hier haben, ist wieder eine Main-Methode, die eine Instanz von dieser Klasse swallowed exceptions erstellt und dann die dood-Methode aufruft. Und was dood jetzt macht, ist von einer bestimmten Datei etwas zu lesen, nämlich dieses f.txt. Und dabei kann natürlich was schiefgehen, zum Beispiel kann die Datei gar nicht existieren, und deswegen zwingt uns die Java-Sprache, hier so eine Exception zu behandeln, nämlich die FileNodeFoundException. Wenn ich jetzt als Programmierer vielleicht nicht weiß, was mache ich da, dann könnte ich, und das wäre nicht gut, einfach so einen leeren Exception-Händler hier hinschreiben, wo im Prinzip nichts drinsteht. Und was bedeutet das? Das bedeutet, falls diese Datei zum Beispiel nicht existiert oder aus anderen Gründen nicht gelesen werden kann, springt der Kontrollfluss dann direkt hierher. Also wir werden nicht den Code, der hier noch kommt, ausführen, sondern wir springen direkt hierher und kehren dann aber ganz regulär von der Methode dood zurück. Und anschließend folgt aber dann ein Code, der ja davon ausgeht, dass diese Datei gelesen wurde, und wir würden diesen Code dann trotzdem ausführen. Das heißt, wir sind dann in einem eigentlich illegalen Zustand und führen aber einfach das Programm weiter aus, und das ist in der Regel nicht das, was man möchte. Das heißt, diese leeren Catch-Blocks, wie man sie hier sieht, bitte nicht machen, sondern stattdessen immer diese Exception in irgendeiner Form behandeln. Und eine Variante, die man in Java immer machen kann, ist, dass man diese Exception, die hier zum Beispiel eine FileNodeFoundException ist, in eine andere Art von Exception umwandelt, die dann nicht wieder explizit behandelt werden muss, nämlich eine Run-Time-Exception. Und was das dann bewirken würde, ist, dass wenn wir den Code ausführen und diese Exception tatsächlich auftritt, die dann auch hier auftritt, und das Programm an der Stelle dann abgebrochen wird und uns ausgibt, oder war eine Run-Time-Exception, die eigentlich eine FileNodeFoundException gewrappt hat, und wir sozusagen sehen, dass da was schief gelaufen ist, anstatt einfach diesen Code auszuführen, der davon ausgeht, dass die Datei ja gelesen wurde. Um für aufrufenden Code etwas klarer zu machen, welche Exceptions vielleicht geworfen werden können, wenn ich eine Subroutine oder Funktion aufrufe, erlauben manche Programmiersprachen, dass die potenziell geworfenen Exceptions als Teil des Headers der Funktion einfach mitdefiniert werden. Das ist zum Beispiel in manchen Sprachen sogar Pflicht, also in Modular 3 zum Beispiel, muss jede Exception, die eventuell geworfen werden, könnte explizit deklariert sein, um zu sagen, hey, wenn du diese Funktion aufrufst, könnte eventuell diese Exception auftreten, also behandeln sie bitte. In anderen Sprachen wie zum Beispiel C++ ist das optional, das heißt, eine Funktion oder Methode kann angeben, dass einer Exception auftreten könnte, muss es aber nicht machen und dann hängt so ein bisschen von den Konventionen im jeweiligen Projekt ab, für welche Exceptions man das tatsächlich machen sollte. Eine dritte Variante ist, und das ist das, was Java macht, dass bestimmte Arten von Exceptions deklariert werden müssen, andere aber eben nicht. Man spricht da an Java von checked und unchecked Exceptions. Also für checked Exceptions muss deklariert werden, dass die geworfen wird, und für unchecked Exceptions ist das Ganze optional. In dem Beispiel, was wir gerade gesehen haben, ist die FileNotFoundException, zum Beispiel eine checked Exception, das heißt, wir müssen entweder die Exception behandeln oder deklarieren, dass die in einer bestimmten Funktion geworfen werden könnte, wohingegen die runtimeException, in die ich die FileNotFoundException dann ja gewerbt hatte, eine unchecked Exception ist, das heißt, das muss nicht explizit deklariert werden und muss auch nicht explizit behandelt werden, sondern wir können die einfach bis zur MainMethode durchpropagieren lassen und das Programm würde dann halt dann abbrechen. In manchen Sprachen gibt es zusätzlich zu den eigentlichen Exception-Handlers auch nur eine Möglichkeit, sogenannte Cleanup Operations, zu definieren, also bestimmtes Verhalten, was ausgeführt werden soll, wenn ein Exception geworfen würde oder auch wenn sie nicht geworfen wurde. Also quasi immer, wenn ein Stück Code zu Ende ist, sollen diese Cleanup Operations ausgeführt werden, egal ob eine Exception geworfen wurde oder nicht. Das macht man dann mit den sogenannten Finally-Closes, die gibt es zum Beispiel in Java und die Semantik von denen ist, dass die eben immer ausgeführt werden, wenn der Kontrollfluss ein bestimmtes Stück Code verlässt, egal ob das jetzt durch eine Exception ist oder weil wir einfach am Ende dieses Blogs angekommen sind. Was macht man damit üblicherweise? Also das ist ja nützlich, um den lokalen Zustand aufzuräumen und um zum Beispiel Ressourcen wieder freizugeben, wie geöffnete Dateien oder vielleicht irgendwelche Stockets für Netzwerkverbindung. So, zum Abschluss habe ich wieder ein kleines Quiz und zwar zur Abwechslung diesmal mit Python, eine Sprache, die auch Exceptions hat und die eben all die Konstrukte, die wir gerade gesehen haben, also Try und Catch und Finally Hat. Catch heißt hier Accept, aber ansonsten ist das im Prinzip dieselbe Idee, die wir ja gerade schon gesehen haben. Also hier ist ein Stück Python-Code und die Frage ist, was gibt dieser Code schlussendlich aus, wenn er ausgeführt wird und ich würde Sie wie immer bei den Quizzes bitten, das Video hier kurz anzuhalten, selbst darüber nachzudenken, in Ilias abzustimmen und dann erst die Lösung anzuschauen. So, schauen wir mal, was hier rauskommt. Also das richtige Ergebnis wäre A, D, I, C und zwar aus folgendem Grund. Also hier unten geht es los, das ist sozusagen die Main-Methode oder ja der Hauptteil dieses kleinen Python-Scripts, wo wir F aufrufen. In F haben wir einen Try-Block, in dem wir erst mal A ausgeben. Das A auszugeben wirft keine Exception, das heißt, wir gehen nicht in diesen Exception-Händler rein, sondern kommen anschließend direkt hier beim Finally raus. Finally ruft als erstes die andere Funktion auf, nämlich G, das heißt, wir sind jetzt hier. In G haben wir wieder einen Try-Block, in dem wir eine Exception erstellen und dann noch gleich werfen, also dieses Race-Oops, erstellt einen Exception oder benutzt einen String als Exception und wirft diese Exception dann. Deswegen kommen wir hier in diesen Exception-Händler, der dann D ausgibt. Also wir haben jetzt A ausgegeben und dann als nächstes D. Haben anschließend noch den Finally-Block, der immer aufgerufen wird, wenn wir aus diesem Try-Block hier oben aus dem rausgehen, das heißt, wir geben anschließend noch E aus und kehren dann hierhin zurück, haben jetzt G aufgerufen und sind aber noch im zweiten Teil dieses Finally-Blocks und geben deswegen schlussendlich auch noch C aus und somit kommt also A, D, E, C raus. Ja, und dann sind wir auch am Ende dieses Dritten von fünf Teilen im Modul Control Abstraction. Sie wissen jetzt hoffentlich ein bisschen besser darüber Bescheid, was Exceptions sind und wie die in verschiedenen Programmiersprachen benutzt werden können und was die Semantik davon ist, wenn Exceptions geworfen werden, gefangen werden, propagiert werden und so weiter. Vielen Dank fürs Zuhören und bis zum nächsten Mal.