Wozu dienen try-catch-Anweisungen?

Try-catch-Anweisungen dienen in Zusammenarbeit mit Exceptions hauptsächlich dem Abfangen von Programmfehlern.

Wie in anderen Programmen auch, so können in Java-Programmen natürlich Fehler auftreten. Nach ihrem Schweregrad werden sie in zwei Hauptkategorien eingeteilt:

  1. Errors sind Fehler, die meist durch Fehlfunktionen der JVM ausgelöst werden, zum sofortigen Abbruch der Programmausführung führen und nicht abgefangen werden.
  2. Exceptions (Ausnahmen) sind Fehler, die meist durch das Programm selbst (Konvertierungsprobleme, Typisierungsfehler etc.) oder unvorhergesehene Änderungen der Ausführungsbedingungen (Netzwerkabriss, Dateisystemfehler, etc.) ausgelöst werden können und gesichert abgefangen werden müssen. Hierzu dienen try-catch-Blöcke.

Der folgende Codeausschnitt zeigt den Einsatz des try-catch-Blockes am Beispiel des Aufrufs des Default-Browsers, mit dem hier die Webseite von Javabeginners dargestellt wird.

try {
    Desktop.getDesktop().browse(new URI("http://www.javabeginners.de"));
} catch (IOException ioe) {
    System.out.println("IO-Problem");
    ioe.printStackTrace();
} catch  (URISyntaxException use) {
    System.out.println("URI-Problem");
    use.printStackTrace();
}
System.out.println("weiter geht's...");

Innerhalb des try-Blockes wird die gewünschte Anweisung formuliert, die ggf. zu Fehlern führen kann. Bei der Ausführung des obigen Codes besteht die Möglichkeit, dass im Wesentlichen zweierlei Fehler auftreten: Beim Netzwerkverkehr könnten Probleme bestehen oder die Auflösung des URI könnte fehlschlagen.
In beiden Fällen wird jeweils eine spezielle Exception ausgelöst ("geworfen"), die innerhalb eines eigenen catch-Blockes abgefangen werden muss.

v.7.0 Gelegentlich kommt es wie oben vor, dass nicht nur eine, sondern mehrere Exceptions geworfen werden können. Deren Abfangen führt zu einer unschönen und schlecht lesbaren Folge von catch-Anweisungen. Ab Java 7.0 kann dies durch eine Multi-catch genannte Zusammenfassung der Exceptiontypen vereinfacht werden, wie im folgenden Code-Abschnitt gezeigt. Als Argument der catch-Anweisung werden hierbei die abzufangenden Exceptiontypen durch ein Pipe-Zeichen '|' getrennt.

try {
    Desktop.getDesktop().browse(new URI("htp://www.javabeginners.de"));
} catch (IOException | URISyntaxException e) {
    e.printStackTrace();
    System.out.println("IO- oder URI-Problem");
}
System.out.println("weiter geht's...");

Wie die Ausgabeanweisungen hier zeigen, können innerhalb des catch-Blockes beliebige Maßnahmen ergriffen werden, die im Falle einer Exception durchgeführt werden sollen.
Zur Demonstration der Auslösung einer Exception kann z.B. die Protokollangabe des URL fehlerhaft verändert werden, etwa von http:// zu htp://. Die Folge ist eine IOException, die verhindert, dass der Browser geöffnet wird. Gleichzeitig wird die folgende Ausgabe generiert:

java.io.IOException: Failed to mail or browse htp://www.javabeginners.de. Error code: -10814
	at sun.lwawt.macosx.CDesktopPeer.lsOpen(CDesktopPeer.java:72)
	at sun.lwawt.macosx.CDesktopPeer.browse(CDesktopPeer.java:65)
	at java.awt.Desktop.browse(Desktop.java:386)
	at MultiCatch.main(MultiCatch.java:12)
IO-Problem
weiter geht's...

Man erkennt hier an der Ausgabe von "weiter geht's...", dass das Programm nach dem Abfangen der Ausnahme hinter dem try-catch-Block weiter ausgeführt wird. Das Programm wird also nicht beendet.
Diese Tatsache verleitet oft dazu, auf offensichtlich notwendige Sicherungsabfragen im Code zu verzichten und statt dessen einfach eine try-catch-Anweisung zu verwenden. Schauen wir uns den folgenden Code an:

BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
int i = 0;
try {
    i = new Integer(reader.readLine());
} catch (IOException e) {
    e.printStackTrace();
}

Hier wird eine Eingabe von der Konsole gelesen, als int gespeichert und ausgegeben. Das geht so lange gut, wie auch ein numerischer Wert eingegeben wird. Beim Auslesen von alphanumerischen Werten wird hingegen eine java.util.NumberFormatException ausgelöst, da ein solches Literal nicht in einen numerischen Wert gewandelt werden kann. Ginge man nun her und würde diesen Fall lediglich durch einen try-catch-Block absichern, so wären zwar Fehler abgesichert, ein sinnvoller Programmablauf selbst wäre jedoch nicht unbedingt gegeben, da die Applikation ohne Behandlung der Eingabe weiterlaufen würde. Die falsche Belegung der Variablen könnte im weiteren Verlauf u.U. zu schwerwiegenden Fehlfunktionen führen.

Im gezeigten Fall sollte somit unbedingt eine Prüfung des Eingabetyps vorangehen und eine Fehleingabe möglichst korrigiert werden können. Dies kann, je nach Einzelfall, auf unterschiedliche Weise erfolgen, etwa durch entsprechende Verwendung eines Scanner-Objektes oder auch durch eine Prüfung über reguläre Ausdrücke. Sie wird hier jedoch der Übersichtlichkeit halber in der Folge nicht berücksichtigt.

Das Beispiel demonstriert jedoch auch, dass Fehler die beim Einlesen der Eingabe auftreten können, über eine IOException abgefangen werden müssen. Wird nicht wie hier vom Standard-Eingabestrom gelesen, sondern z.B. aus einer Datei, so muss der Strom nach seiner Nutzung wieder geschlossen werden. (Der Standard-Eingabestrom ist und bleibt immer offen.)

BufferedReader reader = null;
try {
    reader = new BufferedReader(new FileReader("test.txt"));
    int i = new Integer(reader.readLine());
    reader.close();
} catch (IOException e) {
    e.printStackTrace();
}

Dies geschieht hier innerhalb des try-Blockes und ist aus diesem Grunde nicht unproblematisch, da er im Fehlerfall nicht vollständig ausgeführt und somit der Stream u.U. auch nicht geschlossen wird.
Hier kann ein dritter Block an das try-catch-Statement angeschlossen werden, der mit dem Schlüsselwort finally eingeleitet wird:

BufferedReader reader = null;
try {
    reader = new BufferedReader(new FileReader("test.txt"));
    int i = new Integer(reader.readLine());
} catch (IOException e) {
    e.printStackTrace();
} finally {
    try {
        reader.close();
    } catch (IOException e) {}
}

Dieser optionale Abschnitt wird immer und auf jeden Fall ausgeführt, auch wenn die try-Anweisung fehlschlägt, etwa weil in deren Verlauf ein Fehler auftritt. Hier ist der geeignete Ort, um Ressourcen freizugeben, Dateien zu schließen, die u.U. bei der Ausführung des try-Blockes geöffnet wurden oder wie hier Streams wieder zu schließen. Da die Methode close() ihrerseits eine IOException auslösen kann, muss sie ebenfalls in einen gesonderten try-catch-Block eingeschlossen werden, leider eine ziemlich unübersichtliche Konstruktion, die jedoch seit Java 7.0 durch das try-with-resources Statement zum Glück deutlich vereinfacht werden kann.