9.4 Abschlussbehandlung mit finally
Neben catch gibt es einen anderen Block, der bei try verwendet werden kann: finally. Er dient zur Abschlussbehandlung und eignet sich gut für Ressourcenfreigaben.
In den folgenden Beispielen wollen wir die Ausmaße eines GIF-Bildes auslesen und dabei lernen, wie die Datei korrekt geschlossen wird. Das Grafikformat GIF ist sehr einfach und gut dokumentiert, etwa unter https://www.fileformat.info/format/gif/egff.htm. Dort lässt sich erfahren, wie sich die Ausmaße ganz einfach im Kopf einer GIF-Datei ablesen lassen, denn nach den ersten Bytes 'G', 'I', 'F', '8', '7' (oder '9'), 'a' folgen in 2 Byte an Position 6 und 7 die Breite und an Position 8 und 9 die Höhe des Bildes.
Die ignorante Version
In der ersten Variante schreiben wir den Algorithmus einfach herunter und kümmern uns nicht um die Fehlerbehandlung; mögliche Ausnahmen leitet die statische main(…)-Methode an die JVM weiter:
import java.io.*;
public class ReadGifSizeIgnoringExceptions {
public static void main( String[] args )
throws FileNotFoundException, IOException {
RandomAccessFile raf = new RandomAccessFile( "duke.gif", "r" );
raf.seek( 6 );
System.out.printf( "%s x %s Pixel%n", raf.read() + raf.read() * 256,
raf.read() + raf.read() * 256 );
}
}
In der Klasse haben wir eine Kleinigkeit noch nicht beachtet: das Schließen des Datenstroms. Das Programm endet mit dem Auslesen der Bytes, aber das Schließen mit close() fehlt. (Das Programm ist klein, und die JVM gibt nach dem Programmende alle nativen Betriebssystemressourcen wieder frei. Da unser Java-Programm aber länger laufen kann, ist es guter Stil, nach dem Abschluss der Dateioperationen Ressourcen zu schließen.) Nehmen wir eine Zeile nach der Konsolenausgabe hinzu:
...
System.out.printf( "%s x %s Pixel%n", raf.read() + raf.read() * 256,
raf.read() + raf.read() * 256 );
raf.close();
Das close() wiederum kann auch eine IOException auslösen, die jedoch schon über throws in der main-Signatur angekündigt wurde. Natürlich können wir throws FileNotFoundException, IOException wieder zu throws IOException abkürzen.
Der gut gemeinte Versuch
Dass ein Programm in unserem Fall den main-Thread und damit auch die JVM beendet, sobald eine Datei nicht da ist, ist ein bisschen hart. Daher wollen wir ein try-catch formulieren und die Ausnahme ordentlich abfangen und dokumentieren:
import java.io.*;
public class ReadGifSizeCatchingExceptions {
public static void main( String[] args ) {
try {
RandomAccessFile raf = new RandomAccessFile( "duke.gif", "r" );
raf.seek( 6 );
System.out.printf( "%s x %s Pixel%n", raf.read() + raf.read() * 256,
raf.read() + raf.read() * 256 );
raf.close();
}
catch ( FileNotFoundException e ) {
System.err.println( "Datei ist nicht vorhanden!" );
}
catch ( IOException e ) {
System.err.println( "Allgemeiner Ein-/Ausgabefehler!" );
}
}
}
Ist damit alles in Ordnung?
Ab jetzt wird scharf geschlossen
Nehmen wir an, das Öffnen führt zu keiner Ausnahme, doch beim Zugriff auf ein Byte kommt es unerwartet zu einer Ausnahme. Das read() wird abgebrochen, und die JVM leitet uns in den Exception-Block, der eine Meldung ausgibt. Das Problem: Dann schließt das Programm den Datenstrom nicht. Wir könnten verleitet werden, in den catch-Zweig auch ein close() zu schreiben, doch ist das eine Quellcode-Duplizierung, die wir vermeiden müssen. Hier kommt ein finally-Block zum Zuge.
finally-Blöcke stehen immer hinter catch-Blöcken, und ihre wichtigste Eigenschaft ist die, dass der Programmcode im finally-Block immer ausgeführt wird, egal, ob es eine Ausnahme gab oder ob es keine Ausnahme gab und die Routine glatt durchlief. Das ist genau, was wir hier bei der Ressourcenfreigabe brauchen. Da finally immer ausgeführt wird, wird die Datei geschlossen (und der interne File-Handle freigegeben), wenn alles gut ging – und ebenso im Fall einer Ausnahme:
RandomAccessFile raf = null;
try {
raf = new RandomAccessFile( "duke.gif", "r" );
raf.seek( 6 );
System.out.printf( "%s x %s Pixel%n", raf.read() + raf.read() * 256,
raf.read() + raf.read() * 256 );
}
catch ( FileNotFoundException e ) {
System.err.println( "Datei ist nicht vorhanden!" );
}
catch ( IOException e ) {
System.err.println( "Allgemeiner Ein-/Ausgabefehler!" );
}
finally {
if ( raf != null )
try { raf.close(); } catch ( IOException e ) { }
}
Da close() eine IOException auslösen kann, muss der Aufruf selbst mit einem try-catch ummantelt werden. Das führt zu etwas abschreckenden Konstruktionen, die TCFTC (try-catch-finally-try-catch) genannt werden. Ein zweiter Schönheitsfehler ist der, dass die Variable raf nun außerhalb des try-Blocks deklariert werden muss. Das gibt ihr als lokaler Variablen einen größeren Radius – größer, als er eigentlich sein sollte. Mit einem Extrablock lässt sich das lösen; es sieht aber nicht so hübsch aus. Das spezielle Sprachkonstrukt try mit Ressourcen löst das elegant; Informationen dazu folgen in Abschnitt 9.6.1, »try mit Ressourcen«.
[»] Hinweis
In einem try-Block deklarierte lokale Variablen sind nicht im angehängten catch- oder finally-Block sichtbar.
Zusammenfassung
Nach einem catch (oder mehreren) kann optional ein finally-Block folgen. Die Laufzeitumgebung führt die Anweisungen im finally-Block immer aus, egal, ob eine Ausnahme auftrat oder die Anweisungen im try-Block optimal durchliefen. Das heißt, der Block wird auf jeden Fall ausgeführt – lassen wir System.exit(int) oder Systemfehler einmal außen vor –, auch wenn im try-Block ein return, break oder continue steht oder eine Anweisung eine neue Ausnahme auslöst. Der Programmcode im finally-Block bekommt auch gar nicht mit, ob vorher eine Ausnahme auftrat oder alles glattlief. Wenn das von Interesse ist, müsste eine Anweisung am Ende des try-Blocks ein Flag belegen, was ein Ausdruck im finally-Block dann testen kann.
Sinnvoll sind Anweisungen im finally-Block immer dann, wenn Operationen stets ausgeführt werden sollen. Eine typische Anwendung ist die Freigabe von Ressourcen wie das Schließen von Dateien.
[»] Hinweis
Es gibt bei Objekten einen Finalizer, doch der hat mit finally nichts zu tun. Der Finalizer ist eine besondere Methode, die immer dann aufgerufen wird, wenn der Garbage-Collector ein Objekt wegräumt. Mittlerweile sind Finalizer veraltet.
Ein try ohne catch, aber ein try-finally
Ein try-Block fängt immer Ausnahmen ab, doch nicht zwingend muss ein angehängter catch-Block diese behandeln; throws kann die Ausnahmen einfach nach oben weiterleiten. Nur eine Konstruktion der Art try {} ohne catch ist ungültig, jedoch ist ein try-Block ohne catch, aber mit finally absolut legitim. Diese Konstruktion ist in Java gar nicht so selten, denn sie ist wichtig, wenn eben keine Ausnahme behandelt werden soll, aber unabhängig von möglichen Ausnahmen immer Programmcode abgearbeitet werden soll – ein typisches Beispiel ist die Ressourcenfreigabe.
Kommen wir zu unserem Programm zurück, das die Größe eines GIF-Bildes ermittelt. Wenn beim IO-Fehler nichts zu retten ist, geben wir die Ausnahme an den Aufrufer weiter, versäumen aber nicht, die in der Methode angeforderten Ressourcen wieder freizugeben:
import java.io.*;
public class ReadGifSizeWithTryFinally {
public static void printGifSize( String filename )
throws FileNotFoundException, IOException {
RandomAccessFile raf = new RandomAccessFile( filename, "r" );
try {
raf.seek( 6 );
System.out.printf( "%s x %s Pixel%n", raf.read() + raf.read() * 256,
raf.read() + raf.read() * 256 );
}
finally {
raf.close();
}
}
public static void main( String[] args )
throws FileNotFoundException, IOException {
printGifSize( "duke.gif" );
}
}
Anstatt im finally-Block die IOException vom close() selbst zu fangen, leiten wir sie in dieser Implementierung auch mit nach oben, wenn es zu einer Ausnahme beim Schließen kommt. Im Beispiel ReadGifSize.java aus Listing 9.12 hatten wir geschrieben:
if ( raf != null )
try { raf.close(); } catch ( IOException e ) { }
Eine IOException bei close() würde leise versacken, denn der Behandler ist leer. Bei ReadGifSizeWithTryFinally.java wird eine mögliche Ausnahme beim Schließen nach oben geleitet, bei ReadGifSize.java jedoch nicht, denn dort ist der Programmfluss ganz anders.
Aus noch einem Grund ist die Semantik anders, und daher ist von diesem Stil abzuraten, wenn im finally-Block wie bei ReadGifSizeWithTryFinally.java Ausnahmen ausgelöst werden können.
[»] Wichtig: Java-Verhalten bei multiplen Ausnahmen
Kommt es im try-Block zur Ausnahme und löst auch gleichzeitig der finally-Block eine Ausnahme aus, so wird die Ausnahme im try-Block ignoriert – wir sprechen von einer unterdrückten Ausnahme (engl. suppressed exception). In den Zeilen
try {
throw new Error();
}
finally {
System.out.println( "Geht das?" + 1/0 );
}
kommt die im Kontext uninteressante ArithmeticException durch die Division durch null zum Aufrufer, aber sie unterdrückt den viel wichtigeren harten Error.
Gibt es in unserem Beispiel im try-Block eine Ausnahme und ebenso im finally-Block beim Schließen, dann überdeckt die Schließ-Ausnahme jede andere Ausnahme. Nun ist die Ausnahme im try-Block aber in der Regel wichtiger und sollte nicht verschwinden. Um das Problem zu lösen, gibt es ein anderes Sprachmittel, das Abschnitt 9.6, »try mit Ressourcen (automatisches Ressourcen-Management)«, vorstellt.