zum Inhalt springen

Fehlerbehandlung

Während der Ausführung eines Programms kann es passieren, dass eine "unerwartete Situation" eintritt, in der das Programm nicht so ausgeführt werden kann, wie es vom Programmierer beabsichtigt wurde - was soll passieren, wenn aus einer nicht vorhandenen Datei gelesen werden soll, wenn eine Division durch 0 durchgeführt werden soll oder wenn in einem Array auf ein Element an Position -1 zugegriffen werden soll? Das Programm kann nicht weiterarbeiten, aber es sollte vermieden werden, dass das ganze Programm abstürzt und unkontrolliert beendet wird - vielleicht genügt ja auch eine Fehlermeldung und die Aufforderung an den Benutzer, eine Einstellung zu korrigieren o.ä. In älteren Programmiersprachen war die Fehlerbehandlung sehr umständlich, da für jede "anfällige" Anweisung geprüft werden musste, ob sie ausgeführt werden konnte oder nicht - in etwa so:

Eine gute Fehlerbehandlung bei diesem Vorgehen macht den Code jedoch etwas unleserlich, da sehr viele if-else-Anweisungen eingefügt werden müssen, außerdem ist der Programmierer so fast ausschließlich damit beschäftigt, potentielle Fehler zu erkennen und zu behandeln. In der Programmiersprache Java wurde daher ein weiterentwickeltes Konzept zur Fehlerbehandlung eingeführt, das im folgenden vorgestellt werden soll.

# Try-/ Catch-Blöcke

Die Grundidee der Fehlerbehandlung in Java ist dabei sehr anschaulich: Wenn eine Anweisung u.U. nicht ausgeführt werden kann, dann kann der Versuch gestartet werden diese auszuführen - der Versuch kann gelingen, aber auch fehlschlagen. Wenn der Versuch gelingt, so kann "ganz normal" weitergearbeitet werden, andernfalls muss der Programmierer entscheiden, ob, wie und an welcher Stelle das Programm weiter ausgeführt werden kann. Diese Idee wurde mit Hilfe der Schlüsselwörter try und catch umgesetzt: Innerhalb eines try-Blocks stehen Anweisungen, von denen mindestens eine einen Fehler produzieren kann. Unmittelbar nach dem try-Block folgt mindestens ein catch-Block, in dem der Fehler "möglichst gut" behandelt wird:

Wenn in einem try-Block ein Fehler auftritt, so wird dieser verlassen und das Programm "springt" in den entsprechenden catch-Block - in obigem Beispiel würde also die dritte Zeile des try-Blocks nicht mehr ausgeführt (Vgl. Beispielprogramm: Fehlerbehandlung), die Ausgabe des Programms wäre entsprechend:



Es fällt auf, dass das Schlüsselwort catch einen Parameter verlangt, der hier eine Instanz der bisher unbekannten Klasse Exception sein muss - tatsächlich wurde die Behandlung von Fehlern in Java objektorientiert realisiert, d.h. Fehler sind Instanzen von "besonderen" Klassen, und diese Klassen sind in einer eigenen Vererbungshierarchie organisiert (die im Übrigen ein recht gutes Beispiel für die sinnvolle Anwendung des Vererbungs-Prinzips ist).

# Fehlerklassen in Java

Fehler lassen sich grob in zwei Klassen aufteilen: Zum einen gibt es Fehler, die so schwerwiegend sind, dass sich ein Programm nicht mehr sinnvoll ausführen lässt, unabhängig von allen Möglichkeiten zur Fehlerbehandlung - wenn bspw. kein Arbeitsspeicher mehr verfügbar ist, dann kann nicht garantiert werden, dass der catch-Block noch ordnungsgemäß ausgeführt wird, d.h. das Programm muss sofort beendet werden. Diese schwerwiegenden, nicht behandelbaren Fehler werden in Java als Error bezeichnet und auch von einer solchen Klasse abgeleitet. Ihnen stehen die weniger schwerwiegenden Ausnahmen, den s.g. Exceptions, gegenüber, die eine Behandlung des Fehlers zulassen. Exceptions lassen sich wiederum in zwei Gruppen unterteilen: Zum einen gibt es Exceptions wie eine FileNotFoundException, die beim lesenden Zugriff auf eine Datei auftreten kann, und die zwingend abgefangen werden muss. Zum anderen gibt es jedoch auch Fehler, die quasi in jeder Anweisung auftreten könnten (wie bspw. die NullPointerException oder ArrayIndexOutOfBoundsException), so dass es für den Programmierer sehr aufwändig wäre, diese Anweisungen in try-Blöcken ausführen zu lassen - und auch der Übersichtlichkeit des Codes wäre damit nicht gedient. Daher wird noch einmal unterschieden zwischen "normalen" Exceptions und sogenannten RuntimeExceptions, die sich dadurch auszeichnen, dass sie nicht abgefangen werden müssen (Die in obigem Array-Beispiel produzierte Exception gehörte zu den RuntimeExceptions).

Das folgende Diagramm zeigt eine Auswahl der Exception-Klassenhierarchie - Sie sehen, dass die Klassen Error und Exception eine gemeinsame Basisklasse Throwable besitzen, die u.a. die wesentlichen Methoden, die ein Fehler-Objekt bieten muss, implementiert:

 

Je nach Fehlerklasse wird nun ein unterschiedlicher catch-Block ausgeführt (vgl. auch Beispielprogramm: Fehlerbehandlung), wobei die catch-Blöcke von oben nach unten darauf überprüft werden, ob sie für den jeweiligen Exceptiontyp geeignet sind oder nicht. Grundsätzlich wird immer nur ein catch-Block ausgeführt, auch wenn mehrere Blöcke geeignet wären - würde bspw. der letzte Block an erster Stelle stehen, so würde grundsätzlich nur dieser Block ausgeführt, da ArrayIndexOutOfBoundsException, ArithmeticException und ClassCastException von der Klasse Exception abgeleitet sind. Eclipse würde in einem solchen Fall mit der Fehlermeldung:

Unreachable catch block for XYZException. It is already handled by the catch block for Exception

# Das Schlüsselwort finally

# Die Klasse Throwable und das Werfen von Exceptions

Wenn Sie versuchen, eine neue Datei (In Java repräsentiert die Klasse java.io.File eine Datei, wobei eine Datei bspw. ein Verzeichnis oder eine einfache Textdatei sein kann) anlegen zu lassen,

so wird Eclipse Ihnen folgende Fehlermeldung darstellen: Unhandled exception type IOException

Woher aber "weiß" der Compiler, dass hier eine IOException auftreten kann und nicht etwa eine NullPointerException? Ganz einfach: Die Tatsache, dass in der Methode createNewFile() eine IOException auftreten kann, wurde vom Author der Klasse File berücksichtigt und in der Deklaration der Methode angegeben, und zwar mit Hilfe des Schlüsselworts throws:

Durch throws wird in einer Methodendeklaration gekennzeichnet, welche Exceptions durch diese Methode verursacht werden können (dabei können auch mehrere Exceptions durch Kommata getrennt angegeben werden); während mit Hilfe des Schlüsselworts throw innerhalb eines Methodenkörpers an der gewünschten Stelle das "Werfen" einer Exception ausgelöst werden kann, wie folgendes Beispiel demonstriert:

Prinzipiell kann jedes Objekt durch throw geworfen werden, so lange es sich um eine Instanz einer von Throwable (engl. für "werfbar") abgeleiteten Klasse handelt. Daraus folgt, dass es - wie in obigem Beispiel angedeutet - problemlos möglich ist, eigene Exception-Klassen anzulegen und zu verwenden (siehe auch Beispielprogramm: Fehlerbehandlung).

Durch das Schlüsselwort throws ergibt sich eine weitere Möglichkeit, mit fehleranfälligem Code umzugehen: Statt Fehler in einer Methode mit Hilfe von try/catch-Blöcken abzufangen und auf diese zu reagieren, können die Fehler auch "weitergeworfen" werden, indem sie mit Hilfe des throws-Statements in der Methodendeklaration angegeben werden. Die Methode irgendeineMethode() kann also auf zwei Arten aufgerufen werden:

oder...

*