Das Ende der Checked Exception

von Hubert Schmid vom 2013-12-29

Die strukturierte Ausnahmebehandlung in Programmiersprachen ist eine großartige Sache. Im Vergleich zur Fehlerbehandlung mittels Rückgabewerten werden Ausnahmen nicht einfach ignoriert, die Fehlerbehandlung erfolgt getrennt vom eigentlichen Code und nicht behandelte Fehler verursachen minimalen Schaden – zumindest solange man ein paar Programmierrichtlinien einhält.

Als Einzige der verbreiteten Programmiersprachen hat Java dieses Konzept um Checked Exceptions erweitert. Das war gut gemeint, und sollte Anwendungen robuster machen. Nach kurzer Zeit hat sich jedoch gezeigt, dass der Zwang zur Behandlung vor allem zu vielen neuen Problemen führt. Java hat dennoch 20 Jahre an den Checked Exceptions festgehalten. Mittlerweile sind die Schmerzen jedoch so groß, dass eine Abkehr unumgänglich ist. Java 8 macht einen ersten Schritt. Doch bevor ich darauf eingehe, wiederhole ich nochmals die wesentlichen Probleme mit Checked Exceptions.

Falsche Fehlerbehandlung

Das typische Beispiel für falsche Fehlerbehandlung ist der leere catch-Block oder der catch-Block, der nur eine Log-Ausgabe enthält. Vor Kurzem bin ich über ein interessanteres Beispiel gestolpert, das im folgenden Listing zu sehen ist.

synchronized (lock) { while (!isReady()) { try { lock.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }

Dieser Code führt im Falle einer InterruptedException zu einem Deadlock. Anscheinend hat der Entwickler den Unterbrechungsmechanismus nicht hinreichend verstanden. Wie dem auch sei. Wäre InterruptedException keine Checked Exception, dann hätte der Entwickler vermutlich keine try-Anweisung verwendet, und der Code wäre korrekt gewesen. Allgemein kann man beobachten: Keine catch-Blöcke sind besser als falsche catch-Blöcke.

Falsche throws-Deklaration

Viele Entwickler leben noch in der Illusion, dass Methoden nur Checked Exceptions werfen, die durch ihre throws-Deklaration abgedeckt sind. Bis vor 10 Jahren war das noch gar nicht so falsch. Doch spätestens mit der Einführung der Generics konnte daran nicht weiter festgehalten werden, da sich Generics nicht mit Checked Exceptions vertragen.

Das folgende Listing zeigt wie einfach der Mechanismus ausgehebelt wird. Mit der statischen Methode undeclaredThrow können beliebige Ausnahmen geworfen werden, ohne sie in einer throws-Deklaration anzugeben, und ohne ClassCastException zur Laufzeit. Die Checked Exceptions verlieren dadurch ihren Sinn.

public final class Throw { public static void undeclaredThrow(Throwable t) { Throw.<RuntimeException>doUndeclaredThrow(t); } @SuppressWarnings("unchecked") private static <E extends Throwable> void doUndeclaredThrow(Throwable t) throws E { throw (E) t; } }

Funktionales Hindernis

Besonders unhandlich sind Checked Exceptions im Zusammenspiel mit Lambda-Ausdrücken und Methoden-Referenzen. Um einen Lambda-Ausdruck oder eine Methoden-Referenz verwenden zu können, wird zunächst ein Functional Interface mit passender Signatur benötigt. Java 8 stellt solche Definitionen für Signaturen mit bis zu zwei Parametern bereit. Dafür finden sich allein im Paket java.util.function 44 Schnittstellen, die allesamt keine Checked Exceptions unterstützen. Doch es geht nicht nur um die Anzahl der Functional Interfaces sondern auch um die Signatur der Methoden, die sie verwenden. Wie sollte beispielsweise die Methode Iterable.forEach deklariert sein, damit sie auch mit Checked Exceptions funktioniert?

public String concat(List<String> tokens) { Writer writer = new StringWriter(); tokens.forEach(writer::append); // ERROR: incompatible thrown types // IOException in method reference return writer.toString(); }

Da die forEach-Methode eine Consumer<T>-Instanz erwartet, die keine Checked Exception wirft, müsste der Code wie folgt umgeschrieben werden. Das führt die Verwendung funktionaler Paradigmen offensichtlich ad absurdum.

public String concat(List<String> tokens) { Writer writer = new StringWriter(); tokens.forEach(token -> { try { writer.append(token); } catch (IOException e) { throw new WrappedIOException(e); } }); return writer.toString(); }

Was Java 8 im ersten Schritt macht

Java 8 nimmt langsam Abschied von den Checked Exceptions. Insbesondere im Zusammenhang mit Lambda-Ausdrücken und Methoden-Referenzen werden sie zu Bürgern zweiter Klasse degradiert und kaum noch unterstützt. Von den acht neu eingeführten Exceptions der Kernbibliothek ist nur noch die LambdaConversionException eine Checked Exception, wohingegen Konvertierungsfehler wie DateTimeParseException ungeprüft sind. Und zur IOException gesellt sich nun die UncheckedIOException, die in neuem Code verwendet wird, wie beispielsweise in den folgenden Methoden:

Was bis zum Ende fehlt

Der erste Schritt auf dem Weg ist gemacht, doch das Ziel ist noch lange nicht in Sicht. Dabei könnte es so einfach sein: Vollständige Abschaffung der Checked Exceptions. Diese Änderung wäre abwärtskompatibel und würde alle Probleme auf einmal lösen. Mit Java 8 wird das nicht passieren. Aber wünschen kann man ja mal.