Ausnahmen bestätigen die Regel

Aus den unterschiedlichsten Gründen kann es in einem Programm zu einem Fehlverhalten kommen. Ausnahmesituationen sind nicht immer vermeidbar, aber in vielen Fällen sollte man als Programmierer festlegen, wie sich das Programm in einem bestimmten Ausnahmefall verhalten soll. Dieser Blogartikel wird einige grundlegende Konzepte der Fehlerbehandlung in Java, das sogenannte Exception Handling, beschreiben.

Tritt eine Exception auf so bedeutet das, dass Java in diesem Moment nicht mehr mit dem normalen Programmverlauf fortfahren kann, sondern sich gesondert um diese Ausnahmesituation gekümmert werden muss.

Klassifizierung von Exceptions

In Java werden zunächst zwei Arten von Exceptions unterschieden: Checked und Unchecked Exceptions. Generell treten Exceptions erst zur Laufzeit des Programms auf, allerdings muss sich der Programmier explizit um Checked Exceptions kümmern. Deshalb gibt es zur Kompilierungszeit bereits eine entsprechende Meldung (bzw. bereits von der IDE) vor der Ausführung des Programms. Im Gegensatz dazu stehen die Unchecked Exceptions (auch Runtime Exceptions genannt), welche nicht explizit behandelt werden müssen. Tritt eine Runtime Exception ohne explizite Behandlung auf, so bekommt sie eine Standard-Behandlung. Als Unchecked Exception gelten alle Klassen die von der Klasse RuntimeException ableiten. Als Checked Exceptions gelten alle Klassen, die von Exception ableiten, aber nicht von RuntimeException (siehe Abbildung 1). Eine dritte Kategorie, der Error, ist keine Exception im eigentlichen Sinne. Hierbei handelt es sich um eine Art schwerwiegenden Fehler, auf die häufig weder der Programmierer noch der Anwender Einfluss haben, wie zum Beispiel ein OutOfMemoryError. Nichtsdestotrotz könnnen sie genauso im Programmcode abgefangen werden.

exceptions

Abb. 1: Schematische Darstellung zur Klassifizierung von Exceptions

Varianten der Fehlerbehandlung

Grundsätzlich gibt es zwei verschiedene Herangehensweisen zum Abfangen von Exceptions (oder auch Errors). Entweder kümmert sich die Methode, in der die Exception auftritt selbst um den Umgang damit, oder die Verantwortung wird an die aufrufende Methode zurückgegeben. Das Nach-Oben-Reichen der zweiten Varainte wird so lange fortgeführt, bis eine Methode die Ausnahme behandelt.

Als typisches Beispiel für eine Checked Exception sei die Verwendung eines einfachen FileReaders genannt.

Dieser Code wird nicht compilieren. Der Compiler weist den Programmierer bereits darauf hin, dass man sich um eine FileNotFoundException kümmern muss (die IDE meckert das Problem netterweise bereits auch schon an). Der Hintergrund ist, dass der FileReader nicht funktioniert, wenn er die angegebene Datei nicht finden kann. An dieser Stelle muss sich also um die Fehlerbehandlung gekümmert werden. Der Vorteil einer Fehlerbehandlung an dieser Stelle ist, dass das Programm nicht unnötigerweise abstürzt, wenn die gewünschte Datei nicht gefunden wird. Möchte man das Problem zurück an die aufrufende Methode geben, so deklariert man die Exception in der Methode mit dem Schlüsselwort throws.

Damit ist der Compiler (und auch die IDE) zufrieden. Wenn die Datei gefunden wird, läuft der Programmcode normal durch. Wird sie nicht gefunden, dann wird eine FileNotFoundException geworfen und, da die Exception deklariert wurde, an die aufrufende Methode weiter gereicht.

Alternativ kann man sich um das ExceptionHandling an Ort und Stelle kümmern. Dies erfolgt über einen try/catch Block. Der try-Block enthält den Programmcode, in dem eine Exception auftreten könnte. Der catch-Block enthält den Code zur Beschreibung des Programmverhaltens bei einem (bestimmten) Fehler.

Der Vorteil ist, dass das Programm an dieser Stelle nicht einfach abbricht wenn es die angegebene Datei nicht finden kann, sondern man definieren kann, wie es sich weiter verhalten soll. Eine Möglichkeit innerhalb des catch-Blocks bestünde jetzt zum Beispiel darin, einen entsprechenden Vermerk im Logfile zu hinterlassen und den Anwender zu informieren, dass die Datei nicht gefunden werden konnte. Wird die Datei vom FileReader gefunden, dann wird der gesamte Code des try-Blocks normal ausgeführt und anschließend mit dem nächsten Statement nach dem gesamten try/catch-Block fortgefahren. Wird eine FileNotFoundException im try-Block geworfen, so springt das Programm in den catch-Block und führt die dort aufgeführten Befehle aus. Dadurch soll verhindert werden, dass es zu einem Abbruch der gesamten Methode kommt. Voraussetzung dafür ist, dass in dem catch-Block keine weitere Exception geworfen wird.

Es kann auch mehrere catch-Blöcke geben, die das weitere Programmverhalten unterschiedlich steuern, je nach aufgetretener Exception. Verwendet man wie in obigem Beispiel einen FileReader, so sollte dieser auch wieder geschlossen werden, damit es zu keinem Ressourcenleck kommt. Beim Schließen des FileReaders kann es wiederum zu einer IOException kommen, die abgefangen werden muss (die Klasse FileNotFoundException leitet sich von der Klasse IOException ab, somit gehört letztere ebenfalls zu den Checked Exceptions). Eine Erweiterung des Codeschnipsels um einen weiteren catch-Block sieht dann beispielsweise wie folgt aus.

Die Reihenfolge beachten: Wichtig ist hierbei zu wissen, dass die JVM die catch-Blöcke der Reihe nach durchgeht und den Code des ersten Blocks ausführt, welcher die Exception behandeln kann. Damit spielt die Reihenfolge der catch-Blöcke mitunter eine wichtige Rolle. Im vorgestellten Beispiel könnte man die Reihenfolge der catch-Blöcke nicht vertauschen! Da die FileNotFoundException wie bereits erwähnt die IOException erweitert, würde sich die JVM bei der Suche nach einem catch-Block in diesem Fall auch mit der IOException in einem ersten Block begnügen. Damit würde die FileNotFoundException in einem zweiten catch-Block niemals erreicht werden, was dem Programmierer freundlicherweise bereits durch die IDE als „unreachable code“ gekennzeichnet wird. Zur Verdeutlichung folgt hier noch einmal der Programmcode mit vertauschten catch-Blöcken:

Egal welche IOException auftritt, es würde immer der Code aus dem ersten catch-Block ausgeführt werden.

Der finally Block: Optional kann nach den catch-Blöcken noch ein finally-Block verwendet werden. Der Code innerhalb dieses Blocks wird am Ende des gesamten try/catch Statements immer ausgeführt, unabhängig davon ob eine Ausnahmesituation aufgetreten ist oder nicht; sowie ebenfalls unabhängig davon, ob eine Ausnahme abgefangen wurde oder nicht.

Ein finally-Block kann auch direkt auf den try-Block folgen, d.h. es muss kein catch-Block verwendet werden, wenn es einen finally-Block gibt. Ein try-Block kann aber nicht gänzlich ohne catch- und ohne finally-Block verwendet werden. Die einzige Ausnahme, dass der finally-Block nicht ausgeführt wird ist ein vorheriger Aufruf von System.exit(0);

Zusammenfassung

Zusammenfassend sollte man folgende Grundlagen des Exception Handlings in Java kennen:

  • Es gibt Checked und Unchecked Exceptions. Unchecked Exceptions leiten immer von der Klasse RuntimeException ab. Checked Exception leiten von der Klasse Exception ab, aber nicht von RuntimeException.
  • Exceptions kann man einerseits deklarieren, mittels throws Exception-Name im Methodenkopf, was die Handhabung an die aufrufende Methode zurückgibt
  • Exceptions können andererseits selbst behandelt werden, mittels try/catch Blöcken
  • Auf einen try-Block muss mindestens ein catch- oder finally-Block folgen
  • Existiert ein finally-Block, muss kein catch-Block vorhanden sein
  • Existieren mehrere catch-Blöcke, so wird der erste Block ausgeführt, der die Exception behandeln kann
  • Checked Exceptions müssen deklariert oder behandelt werden

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.