Oder die Kunst der Ausnahmenbehandlung

Exception Handling

Eric Schmieders und Dirk Kitscha

Mit Ausnahmenbehandlung verbindet der fachlich Unversierte möglicherweise die spezielle Behandlung von Privatpatienten beim Zahnarzt. Auch könnte er dahinter eine moderne Wellness-Anwendung vermuten, vielleicht eine spezielle Massage. Aber nein, nichts davon ist richtig. Mit Ausnahmenbehandlung ist hier das Exception Handling in Java gemeint, und dabei wird nur eine Sache behandelt, nämlich Fehler, die während der Laufzeit auftreten können und eine besondere Reaktion erfordern.

Fehler beim Programmieren sind unvermeidlich. Schätzungen zufolge besitzt der Source eines jeden Programms mindestens alle 1000 Zeilen einen Entwicklungsfehler. Dazu kommt, dass Fehler während der Programmausführung ihren Ursprung nicht im Programm selbst haben müssen. Fehlerquellen wie plötzlich abbrechende Datenbankverbindungen oder ein nicht mehr adressierbares Dateisystem addieren sich zu den Programmierfehlern. Das ergibt in Summe ein Fehlerpotenzial, dem man bei der Entwicklung oder bei der Refaktorisierung einer Software Beachtung schenken muss.

Die Frage ist also nicht, ob das Programm fehlerfrei ist oder ob überhaupt Fehler auftreten können. Es stellt sich vielmehr die Frage, ob die geplante Architektur zielsicher und tolerant mit Fehlern umzugehen vermag. Bei der Beantwortung dieser Frage soll dieser Artikel unterstützend zur Seite stehen, er führt in das Grundwissen zum Java Exception Handling ein. Die vorgestellten Antipatterns sind Negativbeispiele, die zu vermeidende Fallstricke aufzeigen. Im letzten Drittel wird ein Entwurfsmuster vorgestellt, das eine solide Grundlage zur Ausnahmenbehandlung in Java bietet.

Throwable und die drei Grundtypen

Eine Exception ist ein Ereignis, das während der Ausführung eines Programms auftritt. Dieses Ergebnis unterbricht den normalen Programmablauf. Unter normalem Programmablauf ist der Ablauf ohne das Auftreten von Fehlern und ohne die Mechanismen der Fehlerbehandlung zu verstehen.

Zwar bemüht sich Sun, ein einheitliches Error Handling im Sprachumfang zu verankern, dennoch gibt es mehrere Klassen, die das Fehlerbehandlungskonzept von Java in eine Form gießen. Im Folgenden werden diese Klassen vorgestellt. Alle dedizierten Klassen, die sich dem Error-Handling widmen, haben eine Gemeinsamkeit: Sie leiten alle von der Superklasse Throwable (java.lang.Throwable) ab. Throwable ist die Superklasse aller Fehler-Events, die in Java durch Objekte repräsentiert werden können. Seit Java 1.4 gibt es die Möglichkeit, der Exception deren Grund mit auf den Weg zu geben. Diese Vorrichtung nennt sich Chained Exception Facility und hat den Vorteil, dass man die Quelle einer jeden Exception zu jeder Zeit identifizieren kann. Um einer Exception einen Exception Cause hinzuzufügen, muss die Methode java.lang.Throwable.initCause(Throwable) aufgerufen werden. Die Exception Chain kann man dann per Iteration oder Rekursion durchlaufen und sich zur Quelle durcharbeiten. Die Vererbungshierachie gabelt sich an dieser Stelle. Von Throwable leiten die Klassen Exception und Error ab. Exceptions sind den Kategorien Checked, bzw. Unchecked zuzuordnen. Diese Aufteilung findet allerdings nur auf Compiler-Ebene statt. Die Virtual Machine unterscheidet nicht zwischen diesen beiden Typen.

Checked Exceptions werden in der Methode gefangen (Englisch „catch“), in der die Exception geworfen werden kann, oder an die aufrufende Methode – so möglich – weitergegeben. Wird die Exception weitergeleitet, dann muss sie in der Methodensignatur ausgewiesen werden. Diese zwei Behandlungsmöglichkeiten markieren den wesentlichen strukturellen Unterschied zu den Unchecked Exceptions. Unchecked Exceptions müssen nicht explizit gefangen oder weitergeleitet werden. Ihr Transport durch den Aufrufbaum erfolgt implizit.

Beim Fangen der Exception kann optional ein Finally Block ergänzt werden. Befehle in diesem Block werden auf jeden Fall ausgeführt – auch wenn kein Fehler aufgetreten ist. Folgender Code-Snippet soll einerseits die Syntax demonstrieren, aber auch die Gefahren, die damit einhergehen:

void doIt() throws Exception { 
try { 
return; 
    } finally { 
System.out.println("Ausführung in jedem Fall!"); 
}
}

Das System.out. wird in jedem Fall ausgeführt, selbst wenn im Try-Block ein return erfolgt. Die daraus entstehenden Gefahren werden im Patterns-Teil weiter unten erläutert.

Erbende Klassen können die Methodensignatur aus der Superklasse nicht erweitern. Das gilt auch für das Exception Handling von Checked Exceptions. Das Verändern der Signatur wäre ein Verstoß gegen das Substitutionsprinzip. Eine überladende Methode darf respektive des Substitutionsprinzips die Fehlersignatur der Superklassenmethode übernehmen, die Ausnahmen spezialisieren oder schlicht weglassen [siehe auch hier].

Da mag es Kritiker geben, die diese Art von Exception Handling als allzu müßig empfinden. Schnell könne es passieren, dass man alle Nase lang Exceptions fangen oder weiterleiten müsse. Diese Kritik weist auf einen wichtigen Punkt hin, nämlich auf den vernünftigen Umgang mit Checked Exceptions. Hier kommen die Design Patterns ins Spiel: Dem Exception-Wust können Design Patterns Einhalt gebieten. Bevor wir den Blick auf die Patterns lenken, erläutern wir das Konzept der Unchecked Exceptions.

Unchecked Exceptions müssen weder innerhalb der Klassen gefangen noch in der Methodensignatur ausgewiesen werden. In Java werden Unchecked Exceptions durch die Klasse Runtime (java.lang.Runtime), bzw. durch ihre erbenden Klassen abgebildet.

Ihren Einsatz finden Unchecked Exceptions bei der Andeutung von Programmierfehlern, wie Beispielsweise Kontraktverletzungen, dazu aber später mehr. Wird eine Runtime Exception nicht gefangen, führt sie wie die Checked Exceptions zum Abbruch des Programms oder genauer des Threads.

Der dritte Typus im Bunde ist der Error. Er ist der schwerwiegendste Laufzeitfehler. Er signalisiert VM-Fehler und führt nicht selten zum sofortigen Abbruch der Ausführung, sogar ohne dass Aufräumarbeiten durchgeführt werden können. Ein beliebtes Beispiel ist der OutOfMemoryError (java.lang.OutOfMemory), der auftritt, wenn die VM versucht, zusätzlichen Speicher zu allokieren. In der Regel führt er zum sofortigen Programmabbruch.

Geschrieben von
Eric Schmieders und Dirk Kitscha
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.