Java finally-Block
Bereinigungscode in Java mit finally-Blöcken ausführen, die immer ablaufen – ob eine Ausnahme ausgelöst wurde oder nicht.
Ein finally-Block wird ausgeführt, egal wie der try-Block endet — bei normalem Abschluss, abgefangener Ausnahme, nicht abgefangener Ausnahme oder sogar bei einem vorzeitigen return. Diese Garantie ist sein eigentlicher Zweck: Hier platziert man den Bereinigungscode, der auf jeden Fall ausgeführt werden muss. Das Schließen eines Datei-Handles, das Freigeben einer Sperre, das Wiederherstellen eines Thread-Zustands — alles, dessen Fehlen das Programm in einem schlechteren Zustand hinterlassen würde als zuvor.
Die Struktur
try {
// risky code
} catch (SomeException e) {
// optional — zero or more catches
} finally {
// always runs after the try (and any matching catch)
}finally kann mit catch kombiniert werden, ganz ohne catch (try { ... } finally { ... }), oder mit mehreren catches. Die Teile lassen sich frei kombinieren.
Was „wird immer ausgeführt" bedeutet
Ein finally-Block wird ausgeführt, wenn die Kontrolle den try-Block verlässt, unabhängig davon, wie dies geschieht:
- Normales Ende des
try-Blocks —finallyläuft nach der letzten Anweisung. - Eine aus
trygeworfene Ausnahme —finallyläuft, nachdem das passendecatchabgeschlossen ist (oder, wenn kein catch passt, kurz bevor die Ausnahme weiterpropagiert). returninnerhalb vontryodercatch—finallyläuft bevor der return tatsächlich wirksam wird.breakodercontinue, das dentry-Block verlassen würde —finallyläuft vor dem Sprung.
Die einzigen Möglichkeiten, finally zu überspringen, sind: die JVM selbst stirbt (System.exit, Stromausfall, Runtime.halt), eine Endlosschleife oder ein Deadlock innerhalb des try, oder Thread.stop (das aus genau diesem Grund als veraltet gilt). Für alles, was man in normalem Anwendungscode schreibt, ist finally eine harte Garantie.
try {
return computeAnswer(); // even though there's a return here,
} finally {
cleanup(); // this runs before the method actually returns
}Wofür finally gedacht ist
Die ehrliche Antwort lautet: Ressourcenbereinigung, fast immer. Bevor Java 7 try-with-resources einführte, war die kanonische Form:
InputStream in = null;
try {
in = new FileInputStream(path);
// read from in...
} catch (IOException e) {
// handle
} finally {
if (in != null) {
try { in.close(); } catch (IOException ignored) { /* */ }
}
}Dieses verschachtelte try/catch um close() im finally-Block ist genau jenes Rauschen, das try-with-resources beseitigen sollte. Das sehen wir im nächsten Kapitel. Aber zu verstehen, wofür finally gedacht ist, macht das neuere Konstrukt verständlich.
Über Ressourcen hinaus ist finally nützlich für:
- Wiederherstellen von gemeinsamem Zustand, den man für die Dauer des
tryverändert hat — Erhöhen eines Tiefenzählers, Umschalten eines Flags, Austauschen einesThreadLocal. - Freigeben manuell erworbener Sperren (
Lock.lock()→try { ... } finally { lock.unlock(); }). - Stoppen von Timern oder Schließen von Transaktionen, die
AutoCloseablenicht implementieren.
Wofür finally nicht gedacht ist
Keine Logik schreiben, die Ergebnisse in finally produziert. Der Block läuft unabhängig vom Ergebnis — er weiß nicht, ob der try erfolgreich war. Wenn man commit() in finally platziert, wird auch bei einem Fehler committed.
Und kein return aus finally. Das ist eine der wirklich gefährlichen Ecken der Sprache:
try {
return 1;
} finally {
return 2; // wins — the method returns 2 and the original return is lost
}Das return (oder throw) innerhalb von finally überschreibt jedes return oder jede Ausnahme aus dem try. Die Ausnahme, die gerade weiterpropagieren sollte, wird stillschweigend verworfen. Die meisten Linter markieren dies deshalb als Fehler. Die Regel: finally führt Bereinigung durch; finally produziert keine Werte.
Ausführungsreihenfolge
Wenn sowohl ein catch als auch ein finally vorhanden sind:
try { ... }
catch { ... }
finally { ... }Die Reihenfolge ist genau die, die man erwarten würde: try läuft, wenn es eine Ausnahme wirft und ein catch passt, läuft dieses catch, und finally läuft danach — nach welchem auch immer. Wenn finally dann wirft, ersetzt dieser neue Wurf alles, was gerade aus dem try oder catch propagierte — ein weiterer Grund, finally ruhig zu halten.
Ein ausgearbeitetes Beispiel
Wir instrumentieren eine kleine „Transaktion" mit try/catch/finally und rufen sie auf drei verschiedene Arten auf: normaler Erfolg, ein behebbarer Fehler und ein nicht behebbarer. finally läuft in allen drei Fällen — das ist der ganze Sinn.
Beim dritten Aufruf wirft doWork eine RuntimeException, die das lokale catch nicht abfängt. finally läuft trotzdem und gibt „release lock" aus, bevor die Ausnahme weiter zu main propagiert. Das ist die Eigenschaft, die man von Bereinigungscode erwartet — er hängt nicht davon ab, ob die Behandlung erfolgreich war.
Was als nächstes kommt
Das Muster „Ressource öffnen, damit arbeiten, im finally schließen" ist so häufig, dass Java eine eigene Anweisung dafür eingebaut hat. Weiter zu Java try-with-resources.