Mit diesem Java-Tutorial durchdringen Sie (nicht nur) die Try-Catch-Anweisung.Ringo Chiu | shutterstock.com Wenn Sie schon immer verstehen wollten, wie Fehler im Quellcode dargestellt werden, sollten Sie unbedingt weiterlesen. Wir werfen in diesem Artikel nicht nur einen detaillierten Blick auf Exceptions in Java, sondern auch auf die zugehörigen Sprach-Features. Ganz konkret vermittelt dieses Java-Grundlagen-Tutorial: was Java Exceptions genau sind und welche Typen existieren. wo der Unterschied zwischen „Checked“ und „Unchecked“ Exceptions liegt. drei Möglichkeiten, Exceptions in Java zu werfen. wie try-, catch– und finally-Blöcke eingesetzt werden. Den Quellcode für die Beispiele in diesem Tutorial können Sie hier herunterladen (Zip-Archiv). Was sind Java Exceptions? Wenn das normale Verhalten eines Java-Programms durch unerwartetes Verhalten unterbrochen wird, bezeichnet man diese Abweichung als Exception (Ausnahme). In der Praxis könnte ein Programm beispielsweise versuchen, eine Datei zu öffnen, die nicht existiert – was zu einer Ausnahme führt. Aus programmiertechnischer Sicht handelt es sich bei Java Exceptions um Bibliothekstypen und Sprachfunktionen, die genutzt werden, um Programmfehler im Code darzustellen und zu behandeln. Dabei klassifiziert Java Ausnahmen in einige, wenige Typen: Checked Exceptions, Unchecked oder Runtime Exceptions, sowie Errors (die von der JVM behandelt werden müssen). Checked Exceptions Ausnahmen, die durch externe Faktoren (wie die oben erwähnte, fehlende Datei) entstehen, klassifiziert Java als Checked Exceptions. Der Java Compiler stellt sicher, dass solche Ausnahmen behandelt (korrigiert) werden. Entweder dort, wo sie auftreten oder an anderer Stelle. Unchecked / Runtime Exceptions Versucht ein Programm eine Integer durch 0 zu teilen, demonstriert das einen weiteren Ausnahmen-Typ – die Runtime Exception. Im Gegensatz zur Checked Exception entsteht diese durch schlecht geschriebenen Quellcode und sollten deshalb vom Entwickler behoben werden. Weil der Compiler nicht überprüft, ob Runtime Exceptions behandelt werden oder dokumentiert ist, dass das an anderer Stelle geschieht, spricht man auch von Unchecked Exceptions. Runtime Exceptions Es ist möglich, ein Programm zu modifizieren, um Runtime Exceptions zu behandeln – empfiehlt sich aber, den Quellcode zu korrigieren. Runtime Exceptions entstehen oft, indem ungültige Argumente an die Methoden einer Bibliothek übergeben werden. Errors Ausnahmen, die die Execution-Fähigkeit eines Programms gefährden, sind besonders schwerwiegend. Das könnte beispielsweise der Fall sein, wenn ein Programm versucht, Speicher von der JVM zuzuweisen, aber nicht genügend vorhanden ist, um die Anforderung zu erfüllen. Eine weitere, schwerwiegende Situation tritt auf, wenn ein Programm versucht, ein beschädigtes Classfile über einen Class.forName()-Methodenaufruf zu laden. Eine Exception dieser Art wird als Error bezeichnet. Diese Art der Ausnahme sollte unter keinen Umständen selbst behoben werden – die JVM kann sich davon möglicherweise nicht erholen. Exceptions im Quellcode Eine Ausnahme kann im Quellcode als Error Code oder als Object dargestellt werden. Error Codes vs. Objects Programmiersprachen wie C verwenden Error Codes auf Integer-Basis um Exceptions und ihre Ursachen darzustellen. Hier ein paar Beispiele: if (chdir(“C:temp”)) printf(“Unable to change to temp directory: %dn”, errno); FILE *fp = fopen(“C:tempfoo”); if (fp == NULL) printf(“Unable to open foo: %dn”, errno); Die chdir()-Funktion von C (change directory) gibt eine Ganzzahl zurück: 0 bei Erfolg, -1 bei Misserfolg. Ähnlich verhält es sich mit der fopen()-Funktion (file open), die im Erfolgsfall einen Nonnull-Pointer auf eine FILE-Struktur, anderenfalls einen Null Pointer (dargestellt durch die Konstante NULL) zurückgibt. In beiden Fällen müssen Sie den Integer-basierten Error Code der globalen errno-Variable lesen, um die Ausnahme zu identifizieren, die den Fehler verursacht hat. Error Codes werfen etliche Probleme auf: Ganzzahlen sind bedeutungslos und beschreiben die Exceptions nicht, die sie repräsentieren. Kontext mit einem Error Code zusammenzubringen, ist problematisch. Falls Sie beispielsweise den Namen der Datei ausgeben möchten, die nicht geöffnet werden konnte – wo speichern Sie dann den Dateinamen? Integers sind willkürlich – was beim Konsum von Quellcode verwirren kann. Ein Beispiel: if (!chdir(„C:temp“)) ist klarer als if (chdir(„C:temp“)), um auf schwerwiegende Ausnahmen zu testen. Weil jedoch 0 für Erfolg steht, muss dazu if (chdir(„C:temp“)) spezifiziert werden. Error Codes lassen sich viel zu leicht ignorieren und begünstigen so fehlerhaften Code. Programmierer könnten beispielsweise chdir(„C:temp“); angeben und den if (fp == NULL)-Check ignorieren. Auch errno muss nicht überprüft werden. Um diese Probleme zu vermeiden, wurde bei Java ein neuer Exception-Handling-Ansatz eingeführt. Hierbei werden Objekte kombiniert, die Exceptions mit einem Mechanismus beschreiben, der auf „Throwing“ und „Catching“ basiert. Das bietet im Vergleich zu Error Codes folgende Vorteile: Ein Objekt kann aus einer Klasse mit einem aussagekräftigen Namen erstellt werden. Ein Beispiel ist etwa FileNotFoundException (im java.io-Package). Objects können Kontext in verschiedenen Feldern speichern. Etwa eine Nachricht, einen Dateinamen oder die letzte Position, an der der Parsing-Prozess fehlgeschlagen ist. Um auf Fehler zu testen, kommen keine if-Statements zum Einsatz. Stattdessen werfen („throw“) Sie Exception-Objekte zu einem Handler, der vom Programmcode separiert ist. Das trägt auch dazu bei, den Source Code lesbar zu halten. Exception Handler Bei einem Exception Handler handelt es sich um eine Codesequenz, die eine Ausnahme behandelt. Dabei wird Kontext abgefragt: Die Werte, die zum Zeitpunkt der Ausnahme von den Variablen gespeichert wurden, werden ausgelesen. Die hieraus gewonnenen Informationen nutzt der Exception Handler und ergreift entsprechende Maßnahmen, damit das Java-Programm wieder so läuft wie es soll. Zum Beispiel könnte eine Mitteilung an den Benutzer erfolgen, dass die Datei fehlt. Throwable und seine Unterklassen Java stellt eine Hierarchie von Klassen zur Verfügung, die verschiedene Exception-Arten repräsentieren. Sie sind in der Throwable-Klasse des java.lang-Packages enthalten – inklusive der Subclasses: Exception, Runtime Exception, und Error. Wenn es um Exceptions geht, stellt Throwable quasi die ultimative Superclass dar. Ausschließlich Objekte, die über Throwable und seine Unterklassen erstellt wurden, können geworfen („throw“) und anschließend abgefangen („catch“) werden. Sie werden auch als Throwables bezeichnet. Throwable-Objekte können mit einer Detail Message verknüpft sein, die die Exception beschreibt. Um solche Objects (auch ohne Detail Message) zu erstellen, stehen mehrere Konstruktoren zur Verfügung. Zum Beispiel: erstellt Throwable() ein entsprechendes Objekt ohne Detail Message. Dieser Konstruktor eignet sich für Situationen, in denen kein Kontext vorhanden ist. erstellt Throwable(String message) ein Objekt inklusive Detail Message, die sich protokollieren oder an den User ausgeben lässt. Um die Detail Message zurückzugeben, bietet Throwable (unter anderem) die Methode String getMessage(). Dazu später mehr. Die Exception-Klasse Throwable hat zwei direkte Unterklassen. Exception beschreibt Ausnahmen, die durch einen externen Faktor entstehen. Dabei deklariert Exception dieselben Konstruktoren (mit identischen Parameterlisten) wie Throwable – jeder Konstruktor ruft sein Throwable-Gegenstück auf. Darüber hinaus deklariert Exception keine neuen Methoden, sondern erbt die von Throwable. Java bietet viele Exception-Klassen, die direkt von Exception abgeleitet sind. Zum Beispiel: signalisiert CloneNotSupportedException einen Versuch, ein Objekt zu klonen, dessen Klasse das Cloneable-Interface nicht implementiert. Beide Typen befinden sich im java.lang-Paket. signalisiert IOException, dass ein E/A-Fehler aufgetreten ist. Dieser Typ befindet sich im java.io-Package. signalisiert ParseException, dass beim Text-Parsing ein Fehler aufgetreten ist. Dieser Typ befindet sich im java.text-Paket. Typischerweise werden Subclasses von Exception mit benutzerdefinierten Exception Classes erstellt (deren Namen mit Exception enden sollten). Im Folgenden zwei Beispiele für solche benutzerdefinierten Unterklassen: public class StackFullException extends Exception { } public class EmptyDirectoryException extends Exception { private String directoryName; public EmptyDirectoryException(String message, String directoryName) { super(message); this.directoryName = directoryName; } public String getDirectoryName() { return directoryName; } } Das erste Beispiel beschreibt eine Ausnahmeklasse, die keine Detail Message erfordert. Der Standardkonstruktor ohne Argumente ruft Exception() auf, das wiederum Throwable() aufruft. Das zweite Beispiel beschreibt eine Exception Class, deren Konstruktor eine Detail Message und den Namen des leeren Verzeichnisses erfordert. Der Konstruktor ruft Exception(String message) auf, was wiederum Throwable(String message) aufruft. Objekte, die über Exception oder eine ihrer Unterklassen (mit Ausnahme von RuntimeException oder einer ihrer Unterklassen) instanziiert werden, sind Checked Exceptions. Die RuntimeException-Klasse Bei RuntimeException handelt es sich um eine direkte Unterklasse von Exception. Sie beschreibt eine Ausnahme, die wahrscheinlich durch schlecht geschriebenen Code verursacht wird. RuntimeException deklariert dieselben Konstruktoren (mit identischen Parameterlisten) wie Exception – jeder Konstruktor ruft sein Exception-Gegenstück auf. Auch RuntimeException erbt die Methoden von Throwable. Java bietet diverse Ausnahmeklassen, die direkte Unterklassen von RuntimeException sind. Die folgenden Beispiele sind Bestandteil des java.lang-Packages: ArithmeticException signalisiert eine ungültige arithmetische Operation, wie etwa den Versuch, eine Ganzzahl durch 0 zu teilen. IllegalArgumentException signalisiert, dass ein ungültiges oder unangemessenes Argument an eine Methode übergeben wurde. NullPointerException signalisiert den Versuch, eine Methode aufzurufen oder über die Nullreferenz auf ein Instanzfeld zuzugreifen. Objekte, die von RuntimeException oder einer ihrer Unterklassen instanziiert werden, sind Unchecked Exceptions. Die Error-Klasse Error ist die Throwable-Subklasse, die ein schwerwiegendes (oder auch abnormales) Problem beschreibt. Zum Beispiel: zu wenig Arbeitsspeicher oder der Versuch, eine nicht vorhandene Klasse zu laden. Wie Exception, deklariert auch Error identische Konstruktoren wie Throwable und keine eigenen Methoden. Error Subclasses sind daran zu erkennen, dass ihr Name mit Error endet. Beispiele hierfür (java.lang-Package) sind: OutOfMemoryError, LinkageError und StackOverflowError. Exceptions werfen Zu wissen, wie und wann Ausnahmen ausgelöst werden, ist essenziell, um effektiv mit Java zu arbeiten. Eine Exception zu werfen, umfasst zwei grundlegende Schritte – nämlich: ein Exception-Objekt mit der throw-Anweisung auszulösen, und die throws-Klausel zu verwenden, um den Compiler zu informieren. Das throw-Statement Um Objekte auszulösen, die eine Exception beschreiben, stellt Java das throw-Statement zur Verfügung. Die Syntax: throw throwable; Das durch throwable identifizierte Objekt ist eine Instanz von Throwable oder einer seiner Unterklassen. Normalerweise werden jedoch nur Objekte geworfen, die von Unterklassen von Exception oder RuntimeException instanziiert wurden. Hier einige Beispiele: throw new FileNotFoundException(“unable to find file ” + filename); throw new IllegalArgumentException(“argument passed to count is less than zero”); Das Throwable wird von der aktuellen Methode an die JVM übergeben, die diese auf einen geeigneten Handler überprüft. Wird keiner gefunden, arbeitet die JVM den Method-Call-Stack ab und sucht nach der nächsten Calling-Methode, die die Exception, die vom Throwable beschrieben wird, behandeln kann. Findet sie diese, übergibt sie das Throwable an den Handler der Methode, dessen Code ausgeführt wird, um die Ausnahme zu behandeln. Wird keine Methode gefunden, wird die JVM mit einer entsprechenden Meldung beendet. Die throws-Klausel Wenn Sie eine Checked Exception aus einer Methode werfen möchten, müssen Sie den Compiler informieren. Hierzu ergänzen Sie den Header der Methode um eine throws-Klausel. Die Syntax: throws checkedExceptionClassName (, checkedExceptionClassName)* Eine throws-Klausel besteht aus dem Schlüsselwort throws, gefolgt von einer durch Kommas getrennten Liste der Klassennamen der Checked Exceptions, die aus der Methode geworfen werden. Ein Beispiel: public static void main(String[] args) throws ClassNotFoundException { if (args.length != 1) { System.err.println(“usage: java … classfile”); return; } Class.forName(args[0]); } In diesem Beispiel-Code wird versucht, ein Class File zu laden, das durch ein Befehlszeilenargument identifiziert wurde. Wenn Class.forName() die Datei nicht finden kann, wirft das ein java.lang.ClassNotFoundException-Objekt – also eine Checked Exception. Checked Exceptions sind kontrovers Viele Entwickler werden nicht gerne dazu gezwungen, throws zu spezifizieren oder Checked Exceptions zu behandeln. An dieser Stelle bietet die funktionale Programmierung diverse nützliche Alternativen zu traditionelllen Exception-Handling-Techniken. Nichtsdestotrotz sollten Java-Entwickler wissen, wie Checked Exceptions funktionieren und behandelt werden. Wir werden später weitere Beispiele betrachten. Vorerst sollten Sie diese Regeln verinnerlichen, wenn es darum geht, mit throws-Klauseln zu arbeiten: Wenn irgendwie möglich, sollten Sie die Namen von Unchecked-Exception-Klassen (wie ArithmeticException) nicht in eine throws-Klausel aufnehmen, die ausschließlich für Checked Exceptions gilt. Das verhindert auch, dass der Quellcode unübersichtlich wird. Sie können eine throws-Klausel an einen Konstruktor anhängen und eine Checked Exception aus dem Konstruktor auslösen, wenn bei dessen Ausführung ein Fehler auftritt. Das resultierende Objekt wird nicht erstellt. Wenn eine Superclass-Methode eine throws-Klausel deklariert, muss die überschreibende Subclass-Methode das nicht tun. Wenn die Unterklassen-Methode jedoch eine throws-Klausel deklariert, darf diese keine Namen von Checked-Exception-Klassen enthalten, die nicht auch in der throws-Klausel der Oberklassenmethode enthalten sind – es sei denn, es handelt sich um die Namen von Exception-Unterklassen. Der Name einer Klasse für Checked Exceptions muss nicht in einer throws-Klausel aufgeführt werden, wenn es der Name ihrer Oberklasse ist. Statt throws FileNotFoundException, IOException genügt throws IOException. Der Compiler meldet einen Fehler, wenn eine Methode eine Checked Exception wirft und diese weder behandelt, noch in der throws-Klausel auflistet. Sie können den Namen einer Checked-Exception-Klasse in der throws-Klausel einer Methode deklarieren, ohne eine Instanz dieser Klasse aus der Methode auszulösen. Java verlangt jedoch, dass Sie Code bereitstellen, um diese Ausnahme zu behandeln – auch wenn sie nicht geworfen wird. Manchmal werfen Methoden viele Checked Exceptions. Entsprechend verlockend kann es erscheinen, ein paar Tastenanschläge mit einer throws Exception-Klausel zu sparen. Das macht den Quellcode allerdings weniger gut lesbar. try-Blöcke einsetzen Um Statement-Sequenzen abzugrenzen, die unter Umständen Exceptions werfen, stellt Java den try-Block zur Verfügung. Die Syntax: try { // one or more statements that might throw exceptions } Die Anweisungen in einem try-Block dienen einem gemeinsamen Zweck und können direkt oder indirekt eine Ausnahme auslösen. Ein Beispiel: FileInputStream fis = null; FileOutputStream fos = null; try { fis = new FileInputStream(args[0]); fos = new FileOutputStream(args[1]); int c; while ((c = fis.read()) != -1) fos.write(c); } Dieses Beispiel ist ein Auszug aus einer größeren Java-Copy-Applikation, die eine Quelldatei in eine Zieldatei kopiert. Zu diesem Zweck werden die Klassen FileInputStream und FileOutputStream verwendet (Bestandteil des java.io-Packages). FileInputStream stellt quasi eine Möglichkeit dar, den Byte-Input-Stream einer Datei zu lesen, während FileOutputStream ermöglicht, einen Byte-Output-Stream in eine Datei zu schreiben. Der Konstruktor FileInputStream(String filename) erstellt einen Input Stream für die durch filename identifizierte Datei. Wenn diese Datei nicht existiert, auf ein Verzeichnis verweist oder ein anderes damit zusammenhängendes Problem auftritt, wirft dieser Konstruktor eine FileNotFoundException. Um ein Byte zu lesen und als 32-Bit-Ganzzahl zurückzugeben, bietet FileInputStream eine int read()-Methode. Sie gibt am Ende der Datei -1 zurück. Geht etwas schief, wird eine IOException geworfen. Der Konstruktor FileOutputStream(String filename) erstellt entsprechend einen Output Stream für die durch filename identifizierte Datei. Wenn die Datei auf ein Verzeichnis verweist, nicht existiert, nicht erstellt werden kann oder ein anderes Problem auftaucht, wirft er eine FileNotFoundException. Um ein Byte in die unteren 8 Bits von b zu schreiben, bietet FileOutputStream eine void write(int b)-Methode. Auch hier wird im Nicht-Erfolgsfall eine IOException ausgelöst. Der Großteil des obigen Beispiels besteht aus einem while-Loop. Dieser liest wiederholt das nächste Byte des Input Stream und schreibt dieses in den Output Stream – so lange, bis read() End-of-File signalisiert. Die File-Copy-Logik des try-Blocks ist leicht zu verstehen, weil sie nicht mit Exception-Checking-, Exception-Handling- und Exceptioin-Cleanup-Code kombiniert wird. Wie es sich auswirkt, wenn ein solches Exception-Framework fehlt, zeigt der nachfolgende Beispielcode einer größeren cp-Applikation, die in C geschrieben ist und eine Quell- in eine Zieldatei kopiert: if ((fpsrc = fopen(argv[1], “rb”)) == NULL) { fprintf(stderr, “unable to open %s for readingn”, argv[1]); return; } if ((fpdst = fopen(argv[2], “wb”)) == NULL) { fprintf(stderr, “unable to open %s for writingn”, argv[1]); fclose(fpsrc); return; } while ((c = fgetc(fpsrc)) != EOF) if (fputc(c, fpdst) == EOF) { fprintf(stderr, “unable to write to %sn”, argv[1]); break; } Der File-Copy-Logik in diesem Beispiel zu folgen, ist deutlich schwieriger, weil die oben erwähnten Code-Bestandteile miteinfließen: Die == NULL– und == EOF-Checks entsprechen dabei den verborgenen throw-Anweisungen und den damit verbundenen Prüfungen. Die fprintf()-Funktionsaufrufe sind der Exception-Handling-Code, den das Java-Code-Äquivalent in einem oder mehreren catch-Blöcken ausführen würde. Beim fclose(fpsrc);-Funktionsaufruf handelt es sich um Cleanup Code, der bei Java in einem finally-Block ausgeführt würde. catch-Blöcke verwenden Javas Exception-Handling-Funktion fußt auf catch-Blöcken. Diese kommen zum Einsatz, um eine Sequenz von Anweisungen abzugrenzen, die eine Ausnahme behandeln. Die Syntax: catch (throwableType throwableObject) { // one or more statements that handle an exception } Der catch-Block ähnelt insofern einem Konstruktor, als er eine Parameterliste enthält. Diese Liste besteht jedoch nur aus einem Parameter, der ein Throwable-Typ (Throwable oder eine seiner Unterklassen) ist – gefolgt von einem Identifier für ein Objekt dieses Typs. Tritt eine Exception auf, wird ein Throwable erstellt und an die JVM übergeben, die nach dem nächstgelegenen catch-Block sucht, dessen Parametertyp direkt mit dem des übergebenen Throwable-Objekts übereinstimmt oder dessen Supertyp ist. Findet die JVM diesen Block, übergibt sie das Throwable an den Parameter und führt die Anweisungen des catch-Blocks aus, die das übergebene Throwable abfragen und die Ausnahme anderweitig behandeln können. Ein Beispiel (das das vorherige try-Block-Beispiel erweitert): catch (FileNotFoundException fnfe) { System.err.println(fnfe.getMessage()); } Dieser Code beschreibt einen catch-Block, der Throwables vom Typ FileNotFoundException abfängt und verarbeitet. Nur Throwables, die diesem Typ oder einem Subtyp entsprechen, werden von diesem Block abgefangen. Angenommen, der FileInputStream(String filename)-Konstruktor löst eine FileNotFoundException aus. Dann überprüft die JVM den catch-Block nach try, um festzustellen, ob sein Parametertyp mit dem Typ des Throwables übereinstimmt. Stimmen die Werte überein, übergibt die JVM die Referenz des Throwables an fnfe und überträgt die Execution an den Block. Dieser reagiert, indem er getMessage() aufruft, um die Exception-Message abzurufen, die dann ausgegeben wird. Exceptions in catch-Blöcken auslösen Ein catch-Block kann eine Ausnahme möglicherweise nicht vollständig behandeln – vielleicht muss er auf Informationen zugreifen, die von einer Vorgängermethode im Method-Call-Stack bereitgestellt werden. Falls die Exception teilweise behandelt werden kann, sollte der catch-Block sie erneut auslösen, damit ein Vorgänger-Handler sie abschließend behandeln kann. Eine andere Möglichkeit besteht darin, die Ausnahme für eine spätere Analyse zu protokollieren und sie dann erneut auszulösen. Diese Technik demonstriert folgender Code: catch (FileNotFoundException fnfe) { logger.log(fnfe); throw fnfe; } Mehrere catch-Blöcke angeben Sie können mehrere catch-Blöcke nach einem try-Block spezifizieren. Ein größerer Auszug aus der oben bereits genannten Copy-Applikation: FileInputStream fis = null; FileOutputStream fos = null; { fis = new FileInputStream(args[0]); fos = new FileOutputStream(args[1]); int c; while ((c = fis.read()) != -1) fos.write(c); } catch (FileNotFoundException fnfe) { System.err.println(fnfe.getMessage()); } catch (IOException ioe) { System.err.println(“I/O error: ” + ioe.getMessage()); } Der erste catch-Block behandelt FileNotFoundExceptions, die von einem der beiden Konstruktoren ausgelöst werden. Der zweite catch-Block behandelt IOExceptions, die von den read()– und write()-Methoden ausgelöst werden. Falls Sie mehrere catch-Blöcke angeben, sollten Sie darauf verzichten, einen catch-Block mit einem Supertyp vor einem catch-Block mit einem Subtyp zu spezifizieren. Sehen Sie zum Beispiel davon ab, catch (IOException ioe) vor catch (FileNotFoundException fnfe) zu platzieren. Anderenfalls meldet der Compiler einen Fehler, weil catch (IOException ioe) auch FileNotFoundExceptions behandeln würde und catch (FileNotFoundException fnfe) nie ausgeführt werden könnte. Nicht empfehlenswert ist es außerdem, mehrere catch-Blöcke mit demselben Throwable-Typ zu spezifizieren – auch das führt zu Compiler-Fehlern. finally-Blöcke nutzen Unabhängig davon, ob eine Ausnahme behandelt wird oder nicht, fallen möglicherweise Cleanup-Aufgaben an – beispielsweise, eine geöffnete Datei zu schließen. Für diesen Zweck sieht Java den finally-Block vor. Dieser besteht aus dem Keyword finally – gefolgt von einer durch geschweifte Klammern abgegrenzte Folge von auszuführenden Statements. try-catch-finally-Kontext bereinigen Wenn Ressourcen bereinigt werden müssen und keine Ausnahme aus einer Methode geworfen wird, wird ein finally-Block nach dem letzten catch-Block platziert. Das veranschaulicht der folgende Code-Auszug: FileInputStream fis = null; FileOutputStream fos = null; try { fis = new FileInputStream(args[0]); fos = new FileOutputStream(args[1]); int c; while ((c = fis.read()) != -1) fos.write(c); } catch (FileNotFoundException fnfe) { System.err.println(fnfe.getMessage()); } catch (IOException ioe) { System.err.println(“I/O error: ” + ioe.getMessage()); } finally { if (fis != null) try { fis.close(); } catch (IOException ioe) { // ignore exception } if (fos != null) try { fos.close(); } catch (IOException ioe) { // ignore exception } } Wird der try-Block ohne Exception ausgeführt, wird die Execution an den finally-Block übergeben, um die Input/Output-Streams der Datei zu schließen. Wird eine Ausnahme ausgelöst, wird der finally-Block nach dem entsprechenden catch-Block ausgeführt. FileInputStream und FileOutputStream erben eine void close()-Methode, die IOException auslöst, wenn der Stream nicht geschlossen werden kann. Aus diesem Grund sind sowohl fis.close(); als auch fos.close(); in diesem Beispiel in einen try-Block eingeschlossen. Der zugehörige catch-Block ist hingegen leer, um den gängigen Fehler zu veranschaulichen, der entsteht, wenn Exceptions ignoriert werden. Ein leerer catch-Block, der mit dem entsprechenden Throwable aufgerufen wird, hat keine Möglichkeit, die Ausnahme zu melden. Sie könnten entsprechend jede Menge Zeit damit verschwenden, die Ursache der Exception zu identifizieren, nur um am Ende festzustellen, dass Sie sich das hätten sparen können, wenn der leere catch-Block die Ausnahme gemeldet hätte – und sei es nur in einem Protokoll. try-finally-Kontext bereinigen Wenn Ressourcen aufgeräumt werden müssen und eine Ausnahme aus einer Methode geworfen wird, wird ein finally-Block nach dem try-Block platziert – catch-Blöcke gibt es nicht. Ein weiterer Code-Auszug aus unserer Copy-Anwendung zur Veranschaulichung: public static void main(String[] args) { if (args.length != 2) { System.err.println(“usage: java Copy srcfile dstfile”); return; } try { copy(args[0], args[1]); } catch (IOException ioe) { System.err.println(“I/O error: ” + ioe.getMessage()); } } static void copy(String srcFile, String dstFile) throws IOException { FileInputStream fis = null; FileOutputStream fos = null; try { fis = new FileInputStream(srcFile); fos = new FileOutputStream(dstFile); int c; while ((c = fis.read()) != -1) fos.write(c); } finally { if (fis != null) try { fis.close(); } catch (IOException ioe) { System.err.println(ioe.getMessage()); } if (fos != null) try { fos.close(); } catch (IOException ioe) { System.err.println(ioe.getMessage()); } } } Die File-Copy-Logik wird in diesem Beispiel in eine copy()-Methode verschoben. Diese ist darauf konzipiert, dem Caller eine Exception zu melden – schließt zuvor aber alle geöffneten Dateien. Die throws-Klausel dieser Methode listet nur IOException auf. Dabei ist es ist nicht nötig, FileNotFoundException einzuschließen, weil es eine Unterklasse von IOException darstellt. Auch hier enthält die finally-Klausel eine Menge Code – nur, um zwei Dateien zu schließen. Wie das besser geht, lesen Sie demnächst an anderer Stelle. (fm) Sie wollen weitere interessante Beiträge zu diversen Themen aus der IT-Welt lesen? Unsere kostenlosen Newsletter liefern Ihnen alles, was IT-Profis wissen sollten – direkt in Ihre Inbox!
Java-Tutorial: Exception-Handling-Grundlagen Mit diesem Java-Tutorial durchdringen Sie (nicht nur) die Try-Catch-Anweisung.Ringo Chiu | shutterstock.com
Wenn Sie schon immer verstehen wollten, wie Fehler im Quellcode dargestellt werden, sollten Sie unbedingt weiterlesen. Wir werfen in diesem Artikel nicht nur einen detaillierten Blick auf Exceptions in Java, sondern auch auf die zugehörigen Sprach-Features.
Ganz konkret vermittelt dieses Java-Grundlagen-Tutorial:
was Java Exceptions genau sind und welche Typen existieren.
wo der Unterschied zwischen „Checked“ und „Unchecked“ Exceptions liegt.
drei Möglichkeiten, Exceptions in Java zu werfen.
wie try-, catch– und finally-Blöcke eingesetzt werden.
Den Quellcode für die Beispiele in diesem Tutorial können Sie hier herunterladen (Zip-Archiv).
Was sind Java Exceptions?
Wenn das normale Verhalten eines Java-Programms durch unerwartetes Verhalten unterbrochen wird, bezeichnet man diese Abweichung als Exception (Ausnahme).
In der Praxis könnte ein Programm beispielsweise versuchen, eine Datei zu öffnen, die nicht existiert – was zu einer Ausnahme führt. Aus programmiertechnischer Sicht handelt es sich bei Java Exceptions um Bibliothekstypen und Sprachfunktionen, die genutzt werden, um Programmfehler im Code darzustellen und zu behandeln.
Dabei klassifiziert Java Ausnahmen in einige, wenige Typen:
Checked Exceptions,
Unchecked oder Runtime Exceptions, sowie
Errors (die von der JVM behandelt werden müssen).
Checked Exceptions
Ausnahmen, die durch externe Faktoren (wie die oben erwähnte, fehlende Datei) entstehen, klassifiziert Java als Checked Exceptions. Der Java Compiler stellt sicher, dass solche Ausnahmen behandelt (korrigiert) werden. Entweder dort, wo sie auftreten oder an anderer Stelle.
Unchecked / Runtime Exceptions
Versucht ein Programm eine Integer durch 0 zu teilen, demonstriert das einen weiteren Ausnahmen-Typ – die Runtime Exception. Im Gegensatz zur Checked Exception entsteht diese durch schlecht geschriebenen Quellcode und sollten deshalb vom Entwickler behoben werden. Weil der Compiler nicht überprüft, ob Runtime Exceptions behandelt werden oder dokumentiert ist, dass das an anderer Stelle geschieht, spricht man auch von Unchecked Exceptions.
Runtime Exceptions
Es ist möglich, ein Programm zu modifizieren, um Runtime Exceptions zu behandeln – empfiehlt sich aber, den Quellcode zu korrigieren. Runtime Exceptions entstehen oft, indem ungültige Argumente an die Methoden einer Bibliothek übergeben werden.
Errors
Ausnahmen, die die Execution-Fähigkeit eines Programms gefährden, sind besonders schwerwiegend. Das könnte beispielsweise der Fall sein, wenn ein Programm versucht, Speicher von der JVM zuzuweisen, aber nicht genügend vorhanden ist, um die Anforderung zu erfüllen. Eine weitere, schwerwiegende Situation tritt auf, wenn ein Programm versucht, ein beschädigtes Classfile über einen Class.forName()-Methodenaufruf zu laden.
Eine Exception dieser Art wird als Error bezeichnet. Diese Art der Ausnahme sollte unter keinen Umständen selbst behoben werden – die JVM kann sich davon möglicherweise nicht erholen.
Exceptions im Quellcode
Eine Ausnahme kann im Quellcode als Error Code oder als Object dargestellt werden.
Error Codes vs. Objects
Programmiersprachen wie C verwenden Error Codes auf Integer-Basis um Exceptions und ihre Ursachen darzustellen. Hier ein paar Beispiele:
if (chdir(“C:temp”))
printf(“Unable to change to temp directory: %dn”, errno);
FILE *fp = fopen(“C:tempfoo”);
if (fp == NULL)
printf(“Unable to open foo: %dn”, errno);
Die chdir()-Funktion von C (change directory) gibt eine Ganzzahl zurück: 0 bei Erfolg, -1 bei Misserfolg. Ähnlich verhält es sich mit der fopen()-Funktion (file open), die im Erfolgsfall einen Nonnull-Pointer auf eine FILE-Struktur, anderenfalls einen Null Pointer (dargestellt durch die Konstante NULL) zurückgibt. In beiden Fällen müssen Sie den Integer-basierten Error Code der globalen errno-Variable lesen, um die Ausnahme zu identifizieren, die den Fehler verursacht hat.
Error Codes werfen etliche Probleme auf:
Ganzzahlen sind bedeutungslos und beschreiben die Exceptions nicht, die sie repräsentieren.
Kontext mit einem Error Code zusammenzubringen, ist problematisch. Falls Sie beispielsweise den Namen der Datei ausgeben möchten, die nicht geöffnet werden konnte – wo speichern Sie dann den Dateinamen?
Integers sind willkürlich – was beim Konsum von Quellcode verwirren kann. Ein Beispiel: if (!chdir(„C:temp“)) ist klarer als if (chdir(„C:temp“)), um auf schwerwiegende Ausnahmen zu testen. Weil jedoch 0 für Erfolg steht, muss dazu if (chdir(„C:temp“)) spezifiziert werden.
Error Codes lassen sich viel zu leicht ignorieren und begünstigen so fehlerhaften Code. Programmierer könnten beispielsweise chdir(„C:temp“); angeben und den if (fp == NULL)-Check ignorieren. Auch errno muss nicht überprüft werden.
Um diese Probleme zu vermeiden, wurde bei Java ein neuer Exception-Handling-Ansatz eingeführt. Hierbei werden Objekte kombiniert, die Exceptions mit einem Mechanismus beschreiben, der auf „Throwing“ und „Catching“ basiert. Das bietet im Vergleich zu Error Codes folgende Vorteile:
Ein Objekt kann aus einer Klasse mit einem aussagekräftigen Namen erstellt werden. Ein Beispiel ist etwa FileNotFoundException (im java.io-Package).
Objects können Kontext in verschiedenen Feldern speichern. Etwa eine Nachricht, einen Dateinamen oder die letzte Position, an der der Parsing-Prozess fehlgeschlagen ist.
Um auf Fehler zu testen, kommen keine if-Statements zum Einsatz. Stattdessen werfen („throw“) Sie Exception-Objekte zu einem Handler, der vom Programmcode separiert ist. Das trägt auch dazu bei, den Source Code lesbar zu halten.
Exception Handler
Bei einem Exception Handler handelt es sich um eine Codesequenz, die eine Ausnahme behandelt. Dabei wird Kontext abgefragt: Die Werte, die zum Zeitpunkt der Ausnahme von den Variablen gespeichert wurden, werden ausgelesen. Die hieraus gewonnenen Informationen nutzt der Exception Handler und ergreift entsprechende Maßnahmen, damit das Java-Programm wieder so läuft wie es soll. Zum Beispiel könnte eine Mitteilung an den Benutzer erfolgen, dass die Datei fehlt.
Throwable und seine Unterklassen
Java stellt eine Hierarchie von Klassen zur Verfügung, die verschiedene Exception-Arten repräsentieren. Sie sind in der Throwable-Klasse des java.lang-Packages enthalten – inklusive der Subclasses:
Exception,
Runtime Exception, und
Error.
Wenn es um Exceptions geht, stellt Throwable quasi die ultimative Superclass dar. Ausschließlich Objekte, die über Throwable und seine Unterklassen erstellt wurden, können geworfen („throw“) und anschließend abgefangen („catch“) werden. Sie werden auch als Throwables bezeichnet.
Throwable-Objekte können mit einer Detail Message verknüpft sein, die die Exception beschreibt. Um solche Objects (auch ohne Detail Message) zu erstellen, stehen mehrere Konstruktoren zur Verfügung. Zum Beispiel:
erstellt Throwable() ein entsprechendes Objekt ohne Detail Message. Dieser Konstruktor eignet sich für Situationen, in denen kein Kontext vorhanden ist.
erstellt Throwable(String message) ein Objekt inklusive Detail Message, die sich protokollieren oder an den User ausgeben lässt.
Um die Detail Message zurückzugeben, bietet Throwable (unter anderem) die Methode String getMessage(). Dazu später mehr.
Die Exception-Klasse
Throwable hat zwei direkte Unterklassen. Exception beschreibt Ausnahmen, die durch einen externen Faktor entstehen. Dabei deklariert Exception dieselben Konstruktoren (mit identischen Parameterlisten) wie Throwable – jeder Konstruktor ruft sein Throwable-Gegenstück auf. Darüber hinaus deklariert Exception keine neuen Methoden, sondern erbt die von Throwable.
Java bietet viele Exception-Klassen, die direkt von Exception abgeleitet sind. Zum Beispiel:
signalisiert CloneNotSupportedException einen Versuch, ein Objekt zu klonen, dessen Klasse das Cloneable-Interface nicht implementiert. Beide Typen befinden sich im java.lang-Paket.
signalisiert IOException, dass ein E/A-Fehler aufgetreten ist. Dieser Typ befindet sich im java.io-Package.
signalisiert ParseException, dass beim Text-Parsing ein Fehler aufgetreten ist. Dieser Typ befindet sich im java.text-Paket.
Typischerweise werden Subclasses von Exception mit benutzerdefinierten Exception Classes erstellt (deren Namen mit Exception enden sollten). Im Folgenden zwei Beispiele für solche benutzerdefinierten Unterklassen:
public class StackFullException extends Exception
{
}
public class EmptyDirectoryException extends Exception
{
private String directoryName;
public EmptyDirectoryException(String message, String directoryName)
{
super(message);
this.directoryName = directoryName;
}
public String getDirectoryName()
{
return directoryName;
}
}
Das erste Beispiel beschreibt eine Ausnahmeklasse, die keine Detail Message erfordert. Der Standardkonstruktor ohne Argumente ruft Exception() auf, das wiederum Throwable() aufruft.
Das zweite Beispiel beschreibt eine Exception Class, deren Konstruktor eine Detail Message und den Namen des leeren Verzeichnisses erfordert. Der Konstruktor ruft Exception(String message) auf, was wiederum Throwable(String message) aufruft.
Objekte, die über Exception oder eine ihrer Unterklassen (mit Ausnahme von RuntimeException oder einer ihrer Unterklassen) instanziiert werden, sind Checked Exceptions.
Die RuntimeException-Klasse
Bei RuntimeException handelt es sich um eine direkte Unterklasse von Exception. Sie beschreibt eine Ausnahme, die wahrscheinlich durch schlecht geschriebenen Code verursacht wird. RuntimeException deklariert dieselben Konstruktoren (mit identischen Parameterlisten) wie Exception – jeder Konstruktor ruft sein Exception-Gegenstück auf. Auch RuntimeException erbt die Methoden von Throwable.
Java bietet diverse Ausnahmeklassen, die direkte Unterklassen von RuntimeException sind. Die folgenden Beispiele sind Bestandteil des java.lang-Packages:
ArithmeticException signalisiert eine ungültige arithmetische Operation, wie etwa den Versuch, eine Ganzzahl durch 0 zu teilen.
IllegalArgumentException signalisiert, dass ein ungültiges oder unangemessenes Argument an eine Methode übergeben wurde.
NullPointerException signalisiert den Versuch, eine Methode aufzurufen oder über die Nullreferenz auf ein Instanzfeld zuzugreifen.
Objekte, die von RuntimeException oder einer ihrer Unterklassen instanziiert werden, sind Unchecked Exceptions.
Die Error-Klasse
Error ist die Throwable-Subklasse, die ein schwerwiegendes (oder auch abnormales) Problem beschreibt. Zum Beispiel:
zu wenig Arbeitsspeicher oder
der Versuch, eine nicht vorhandene Klasse zu laden.
Wie Exception, deklariert auch Error identische Konstruktoren wie Throwable und keine eigenen Methoden. Error Subclasses sind daran zu erkennen, dass ihr Name mit Error endet. Beispiele hierfür (java.lang-Package) sind:
OutOfMemoryError,
LinkageError und
StackOverflowError.
Exceptions werfen
Zu wissen, wie und wann Ausnahmen ausgelöst werden, ist essenziell, um effektiv mit Java zu arbeiten. Eine Exception zu werfen, umfasst zwei grundlegende Schritte – nämlich:
ein Exception-Objekt mit der throw-Anweisung auszulösen, und
die throws-Klausel zu verwenden, um den Compiler zu informieren.
Das throw-Statement
Um Objekte auszulösen, die eine Exception beschreiben, stellt Java das throw-Statement zur Verfügung. Die Syntax:
throw throwable;
Das durch throwable identifizierte Objekt ist eine Instanz von Throwable oder einer seiner Unterklassen. Normalerweise werden jedoch nur Objekte geworfen, die von Unterklassen von Exception oder RuntimeException instanziiert wurden. Hier einige Beispiele:
throw new FileNotFoundException(“unable to find file ” + filename);
throw new IllegalArgumentException(“argument passed to count is less than zero”);
Das Throwable wird von der aktuellen Methode an die JVM übergeben, die diese auf einen geeigneten Handler überprüft. Wird keiner gefunden, arbeitet die JVM den Method-Call-Stack ab und sucht nach der nächsten Calling-Methode, die die Exception, die vom Throwable beschrieben wird, behandeln kann. Findet sie diese, übergibt sie das Throwable an den Handler der Methode, dessen Code ausgeführt wird, um die Ausnahme zu behandeln. Wird keine Methode gefunden, wird die JVM mit einer entsprechenden Meldung beendet.
Die throws-Klausel
Wenn Sie eine Checked Exception aus einer Methode werfen möchten, müssen Sie den Compiler informieren. Hierzu ergänzen Sie den Header der Methode um eine throws-Klausel. Die Syntax:
throws checkedExceptionClassName (, checkedExceptionClassName)*
Eine throws-Klausel besteht aus dem Schlüsselwort throws, gefolgt von einer durch Kommas getrennten Liste der Klassennamen der Checked Exceptions, die aus der Methode geworfen werden. Ein Beispiel:
public static void main(String[] args) throws ClassNotFoundException
{
if (args.length != 1)
{
System.err.println(“usage: java … classfile”);
return;
}
Class.forName(args[0]);
}
In diesem Beispiel-Code wird versucht, ein Class File zu laden, das durch ein Befehlszeilenargument identifiziert wurde. Wenn Class.forName() die Datei nicht finden kann, wirft das ein java.lang.ClassNotFoundException-Objekt – also eine Checked Exception.
Checked Exceptions sind kontrovers
Viele Entwickler werden nicht gerne dazu gezwungen, throws zu spezifizieren oder Checked Exceptions zu behandeln. An dieser Stelle bietet die funktionale Programmierung diverse nützliche Alternativen zu traditionelllen Exception-Handling-Techniken. Nichtsdestotrotz sollten Java-Entwickler wissen, wie Checked Exceptions funktionieren und behandelt werden.
Wir werden später weitere Beispiele betrachten. Vorerst sollten Sie diese Regeln verinnerlichen, wenn es darum geht, mit throws-Klauseln zu arbeiten:
Wenn irgendwie möglich, sollten Sie die Namen von Unchecked-Exception-Klassen (wie ArithmeticException) nicht in eine throws-Klausel aufnehmen, die ausschließlich für Checked Exceptions gilt. Das verhindert auch, dass der Quellcode unübersichtlich wird.
Sie können eine throws-Klausel an einen Konstruktor anhängen und eine Checked Exception aus dem Konstruktor auslösen, wenn bei dessen Ausführung ein Fehler auftritt. Das resultierende Objekt wird nicht erstellt.
Wenn eine Superclass-Methode eine throws-Klausel deklariert, muss die überschreibende Subclass-Methode das nicht tun. Wenn die Unterklassen-Methode jedoch eine throws-Klausel deklariert, darf diese keine Namen von Checked-Exception-Klassen enthalten, die nicht auch in der throws-Klausel der Oberklassenmethode enthalten sind – es sei denn, es handelt sich um die Namen von Exception-Unterklassen.
Der Name einer Klasse für Checked Exceptions muss nicht in einer throws-Klausel aufgeführt werden, wenn es der Name ihrer Oberklasse ist. Statt throws FileNotFoundException, IOException genügt throws IOException.
Der Compiler meldet einen Fehler, wenn eine Methode eine Checked Exception wirft und diese weder behandelt, noch in der throws-Klausel auflistet.
Sie können den Namen einer Checked-Exception-Klasse in der throws-Klausel einer Methode deklarieren, ohne eine Instanz dieser Klasse aus der Methode auszulösen. Java verlangt jedoch, dass Sie Code bereitstellen, um diese Ausnahme zu behandeln – auch wenn sie nicht geworfen wird.
Manchmal werfen Methoden viele Checked Exceptions. Entsprechend verlockend kann es erscheinen, ein paar Tastenanschläge mit einer throws Exception-Klausel zu sparen. Das macht den Quellcode allerdings weniger gut lesbar.
try-Blöcke einsetzen
Um Statement-Sequenzen abzugrenzen, die unter Umständen Exceptions werfen, stellt Java den try-Block zur Verfügung. Die Syntax:
try
{
// one or more statements that might throw exceptions
}
Die Anweisungen in einem try-Block dienen einem gemeinsamen Zweck und können direkt oder indirekt eine Ausnahme auslösen. Ein Beispiel:
FileInputStream fis = null;
FileOutputStream fos = null;
try
{
fis = new FileInputStream(args[0]);
fos = new FileOutputStream(args[1]);
int c;
while ((c = fis.read()) != -1)
fos.write(c);
}
Dieses Beispiel ist ein Auszug aus einer größeren Java-Copy-Applikation, die eine Quelldatei in eine Zieldatei kopiert. Zu diesem Zweck werden die Klassen FileInputStream und FileOutputStream verwendet (Bestandteil des java.io-Packages).
FileInputStream stellt quasi eine Möglichkeit dar, den Byte-Input-Stream einer Datei zu lesen, während
FileOutputStream ermöglicht, einen Byte-Output-Stream in eine Datei zu schreiben.
Der Konstruktor FileInputStream(String filename) erstellt einen Input Stream für die durch filename identifizierte Datei. Wenn diese Datei nicht existiert, auf ein Verzeichnis verweist oder ein anderes damit zusammenhängendes Problem auftritt, wirft dieser Konstruktor eine FileNotFoundException. Um ein Byte zu lesen und als 32-Bit-Ganzzahl zurückzugeben, bietet FileInputStream eine int read()-Methode. Sie gibt am Ende der Datei -1 zurück. Geht etwas schief, wird eine IOException geworfen.
Der Konstruktor FileOutputStream(String filename) erstellt entsprechend einen Output Stream für die durch filename identifizierte Datei. Wenn die Datei auf ein Verzeichnis verweist, nicht existiert, nicht erstellt werden kann oder ein anderes Problem auftaucht, wirft er eine FileNotFoundException. Um ein Byte in die unteren 8 Bits von b zu schreiben, bietet FileOutputStream eine void write(int b)-Methode. Auch hier wird im Nicht-Erfolgsfall eine IOException ausgelöst.
Der Großteil des obigen Beispiels besteht aus einem while-Loop. Dieser liest wiederholt das nächste Byte des Input Stream und schreibt dieses in den Output Stream – so lange, bis read() End-of-File signalisiert.
Die File-Copy-Logik des try-Blocks ist leicht zu verstehen, weil sie nicht mit Exception-Checking-, Exception-Handling- und Exceptioin-Cleanup-Code kombiniert wird. Wie es sich auswirkt, wenn ein solches Exception-Framework fehlt, zeigt der nachfolgende Beispielcode einer größeren cp-Applikation, die in C geschrieben ist und eine Quell- in eine Zieldatei kopiert:
if ((fpsrc = fopen(argv[1], “rb”)) == NULL)
{
fprintf(stderr, “unable to open %s for readingn”, argv[1]);
return;
}
if ((fpdst = fopen(argv[2], “wb”)) == NULL)
{
fprintf(stderr, “unable to open %s for writingn”, argv[1]);
fclose(fpsrc);
return;
}
while ((c = fgetc(fpsrc)) != EOF)
if (fputc(c, fpdst) == EOF)
{
fprintf(stderr, “unable to write to %sn”, argv[1]);
break;
}
Der File-Copy-Logik in diesem Beispiel zu folgen, ist deutlich schwieriger, weil die oben erwähnten Code-Bestandteile miteinfließen:
Die == NULL– und == EOF-Checks entsprechen dabei den verborgenen throw-Anweisungen und den damit verbundenen Prüfungen.
Die fprintf()-Funktionsaufrufe sind der Exception-Handling-Code, den das Java-Code-Äquivalent in einem oder mehreren catch-Blöcken ausführen würde.
Beim fclose(fpsrc);-Funktionsaufruf handelt es sich um Cleanup Code, der bei Java in einem finally-Block ausgeführt würde.
catch-Blöcke verwenden
Javas Exception-Handling-Funktion fußt auf catch-Blöcken. Diese kommen zum Einsatz, um eine Sequenz von Anweisungen abzugrenzen, die eine Ausnahme behandeln. Die Syntax:
catch (throwableType throwableObject)
{
// one or more statements that handle an exception
}
Der catch-Block ähnelt insofern einem Konstruktor, als er eine Parameterliste enthält. Diese Liste besteht jedoch nur aus einem Parameter, der ein Throwable-Typ (Throwable oder eine seiner Unterklassen) ist – gefolgt von einem Identifier für ein Objekt dieses Typs.
Tritt eine Exception auf, wird ein Throwable erstellt und an die JVM übergeben, die nach dem nächstgelegenen catch-Block sucht, dessen Parametertyp direkt mit dem des übergebenen Throwable-Objekts übereinstimmt oder dessen Supertyp ist. Findet die JVM diesen Block, übergibt sie das Throwable an den Parameter und führt die Anweisungen des catch-Blocks aus, die das übergebene Throwable abfragen und die Ausnahme anderweitig behandeln können.
Ein Beispiel (das das vorherige try-Block-Beispiel erweitert):
catch (FileNotFoundException fnfe)
{
System.err.println(fnfe.getMessage());
}
Dieser Code beschreibt einen catch-Block, der Throwables vom Typ FileNotFoundException abfängt und verarbeitet. Nur Throwables, die diesem Typ oder einem Subtyp entsprechen, werden von diesem Block abgefangen.
Angenommen, der FileInputStream(String filename)-Konstruktor löst eine FileNotFoundException aus. Dann überprüft die JVM den catch-Block nach try, um festzustellen, ob sein Parametertyp mit dem Typ des Throwables übereinstimmt. Stimmen die Werte überein, übergibt die JVM die Referenz des Throwables an fnfe und überträgt die Execution an den Block. Dieser reagiert, indem er getMessage() aufruft, um die Exception-Message abzurufen, die dann ausgegeben wird.
Exceptions in catch-Blöcken auslösen
Ein catch-Block kann eine Ausnahme möglicherweise nicht vollständig behandeln – vielleicht muss er auf Informationen zugreifen, die von einer Vorgängermethode im Method-Call-Stack bereitgestellt werden. Falls die Exception teilweise behandelt werden kann, sollte der catch-Block sie erneut auslösen, damit ein Vorgänger-Handler sie abschließend behandeln kann. Eine andere Möglichkeit besteht darin, die Ausnahme für eine spätere Analyse zu protokollieren und sie dann erneut auszulösen. Diese Technik demonstriert folgender Code:
catch (FileNotFoundException fnfe)
{
logger.log(fnfe);
throw fnfe;
}
Mehrere catch-Blöcke angeben
Sie können mehrere catch-Blöcke nach einem try-Block spezifizieren. Ein größerer Auszug aus der oben bereits genannten Copy-Applikation:
FileInputStream fis = null;
FileOutputStream fos = null;
{
fis = new FileInputStream(args[0]);
fos = new FileOutputStream(args[1]);
int c;
while ((c = fis.read()) != -1)
fos.write(c);
}
catch (FileNotFoundException fnfe)
{
System.err.println(fnfe.getMessage());
}
catch (IOException ioe)
{
System.err.println(“I/O error: ” + ioe.getMessage());
}
Der erste catch-Block behandelt FileNotFoundExceptions, die von einem der beiden Konstruktoren ausgelöst werden. Der zweite catch-Block behandelt IOExceptions, die von den read()– und write()-Methoden ausgelöst werden.
Falls Sie mehrere catch-Blöcke angeben, sollten Sie darauf verzichten, einen catch-Block mit einem Supertyp vor einem catch-Block mit einem Subtyp zu spezifizieren. Sehen Sie zum Beispiel davon ab, catch (IOException ioe) vor catch (FileNotFoundException fnfe) zu platzieren. Anderenfalls meldet der Compiler einen Fehler, weil catch (IOException ioe) auch FileNotFoundExceptions behandeln würde und catch (FileNotFoundException fnfe) nie ausgeführt werden könnte. Nicht empfehlenswert ist es außerdem, mehrere catch-Blöcke mit demselben Throwable-Typ zu spezifizieren – auch das führt zu Compiler-Fehlern.
finally-Blöcke nutzen
Unabhängig davon, ob eine Ausnahme behandelt wird oder nicht, fallen möglicherweise Cleanup-Aufgaben an – beispielsweise, eine geöffnete Datei zu schließen. Für diesen Zweck sieht Java den finally-Block vor. Dieser besteht aus dem Keyword finally – gefolgt von einer durch geschweifte Klammern abgegrenzte Folge von auszuführenden Statements.
try-catch-finally-Kontext bereinigen
Wenn Ressourcen bereinigt werden müssen und keine Ausnahme aus einer Methode geworfen wird, wird ein finally-Block nach dem letzten catch-Block platziert. Das veranschaulicht der folgende Code-Auszug:
FileInputStream fis = null;
FileOutputStream fos = null;
try
{
fis = new FileInputStream(args[0]);
fos = new FileOutputStream(args[1]);
int c;
while ((c = fis.read()) != -1)
fos.write(c);
}
catch (FileNotFoundException fnfe)
{
System.err.println(fnfe.getMessage());
}
catch (IOException ioe)
{
System.err.println(“I/O error: ” + ioe.getMessage());
}
finally
{
if (fis != null)
try
{
fis.close();
}
catch (IOException ioe)
{
// ignore exception
}
if (fos != null)
try
{
fos.close();
}
catch (IOException ioe)
{
// ignore exception
}
}
Wird der try-Block ohne Exception ausgeführt, wird die Execution an den finally-Block übergeben, um die Input/Output-Streams der Datei zu schließen. Wird eine Ausnahme ausgelöst, wird der finally-Block nach dem entsprechenden catch-Block ausgeführt.
FileInputStream und FileOutputStream erben eine void close()-Methode, die IOException auslöst, wenn der Stream nicht geschlossen werden kann. Aus diesem Grund sind sowohl fis.close(); als auch fos.close(); in diesem Beispiel in einen try-Block eingeschlossen. Der zugehörige catch-Block ist hingegen leer, um den gängigen Fehler zu veranschaulichen, der entsteht, wenn Exceptions ignoriert werden.
Ein leerer catch-Block, der mit dem entsprechenden Throwable aufgerufen wird, hat keine Möglichkeit, die Ausnahme zu melden. Sie könnten entsprechend jede Menge Zeit damit verschwenden, die Ursache der Exception zu identifizieren, nur um am Ende festzustellen, dass Sie sich das hätten sparen können, wenn der leere catch-Block die Ausnahme gemeldet hätte – und sei es nur in einem Protokoll.
try-finally-Kontext bereinigen
Wenn Ressourcen aufgeräumt werden müssen und eine Ausnahme aus einer Methode geworfen wird, wird ein finally-Block nach dem try-Block platziert – catch-Blöcke gibt es nicht. Ein weiterer Code-Auszug aus unserer Copy-Anwendung zur Veranschaulichung:
public static void main(String[] args)
{
if (args.length != 2)
{
System.err.println(“usage: java Copy srcfile dstfile”);
return;
}
try
{
copy(args[0], args[1]);
}
catch (IOException ioe)
{
System.err.println(“I/O error: ” + ioe.getMessage());
}
}
static void copy(String srcFile, String dstFile) throws IOException
{
FileInputStream fis = null;
FileOutputStream fos = null;
try
{
fis = new FileInputStream(srcFile);
fos = new FileOutputStream(dstFile);
int c;
while ((c = fis.read()) != -1)
fos.write(c);
}
finally
{
if (fis != null)
try
{
fis.close();
}
catch (IOException ioe)
{
System.err.println(ioe.getMessage());
}
if (fos != null)
try
{
fos.close();
}
catch (IOException ioe)
{
System.err.println(ioe.getMessage());
}
}
}
Die File-Copy-Logik wird in diesem Beispiel in eine copy()-Methode verschoben. Diese ist darauf konzipiert, dem Caller eine Exception zu melden – schließt zuvor aber alle geöffneten Dateien. Die throws-Klausel dieser Methode listet nur IOException auf. Dabei ist es ist nicht nötig, FileNotFoundException einzuschließen, weil es eine Unterklasse von IOException darstellt. Auch hier enthält die finally-Klausel eine Menge Code – nur, um zwei Dateien zu schließen. Wie das besser geht, lesen Sie demnächst an anderer Stelle. (fm)
Sie wollen weitere interessante Beiträge zu diversen Themen aus der IT-Welt lesen? Unsere kostenlosen Newsletter liefern Ihnen alles, was IT-Profis wissen sollten – direkt in Ihre Inbox!