Lambda-Ausdrücke sind Implementierung von funktionalen Schnittstellen, und bisher haben wir noch nicht die Frage betrachtet was passiert, wenn der Code-Block vom Lambda-Ausdruck eine Ausnahme auslöst und wer diese Auffangen muss.
Ausnahmen im Code-Block eines Lambda-Ausdrucks
In java.util.function gibt es eine funktionale Schnittstelle Predicate, dessen Deklaration im Kern wie folgt ist:
public interface Predicate<T> { boolean test( T t ); }
Ein Predicate führt einen Test durch und liefert wahr oder falsch als Ergebnis. Ein Lambda-Ausdruck kann diese Schnittstelle nun implementieren. Nehmen wir an, wir wollen Testen, ob eine Datei die Länge 0 hat, um etwa Datei-Leichen zu finden. In einer ersten Idee greifen wir auf die existierende Files-Klasse zurück, die size(…)anbietet:
Predicate<Path> isEmptyFile = path -> Files.size( path ) == 0; // N Compilerfehler
Problem dabei ist, das Files.size(…) eine IOException auslöst, die behandelt werden muss und zwar nicht vom Block, in dem der Lambda-Ausdruck als Ganzes steht, sondern vom Code im Lambda-Ausdruck selbst. Das schreibt der Compiler so vor. Folgendes ist also keine Lösung:
try { Predicate<Path> isEmptyFile = path -> Files.size( path ) == 0; // Nee } catch ( IOException e ) { … }
sondern nur:
Predicate<Path> isEmptyFile = path -> { try { return Files.size( path ) == 0; } catch ( IOException e ) { return false; } };
Die Eigenschaft, die Java fehlt, nennt sich Exception-Transparenz, und hier ist deutlich der Unterschied zwischen geprüften und ungeprüften Ausnahmen zu sehen. Bei der Exception-Transparenz wäre kein Ausnahmebehandlung im Lambda-Ausdruck nötig und an einer übergeordneten Stelle möglich. Doch da diese Möglichkeit in Java fehlt, bleibt uns nur übrig, geprüfte Ausnahmen im Lambda-Ausdrücken direkt zu behandeln.
Funktionale Schnittstellen mit throws-Klausel
Ungeprüfte Ausnahmen können immer auftreten und führen (nicht abgefangen) wie üblich zum Abbruch des Threads. Eine throws-Klausel an den Methoden/Konstruktoren ist dafür nicht nötig. Doch können Funktionale Schnittstellen eine throws-Klausel mit geprüften Ausnahmen deklarieren, und die Implementierung einer funktionalen Schnittstelle kann logischerweise geprüfte Ausnahmen auslösen.
Eine Deklaration wie Callable aus dem Paket java.util.concurrent macht das deutlich. (Callable trägt kein @FunctionalInterface):
public interface Callable<V> { V call() throws Exception; }
Das könnte durch folgenden Lambda-Ausdruck realisiert werden:
Callable<Integer> randomDice = () -> (int)(Math.random() * 6) + 1;
Der Aufruf von call() auf einem randomDice muss mit einer Ausnahmebehandlung einher gehen, da call() eine Exception auslöst, etwa so:
try { System.out.println( randomDice.call() ); System.out.println( randomDice.call() ); } catch ( Exception e ) { … }
Dass der Aufrufer die Ausnahme behandeln muss ist klar. Die Deklaration des Lambda-Ausdrucks enthält keinen Hinweis auf die Ausnahme, das ist ein Unterschied zum vorangegangenen Abschnitt.
Design-Tipp
Ausnahmen in Methoden funktionaler Schnittstellen schränken den Nutzen stark ein, und daher löst keine der funktionalen Schnittstellen aus etwa java.util.function eine geprüfte Ausnahme aus. Der Grund ist einfach, denn jeder Methodenaufrufer müsste sonst entweder die Ausnahme weiterleiten oder behandeln.[1]
Um die Einschränkungen und Probleme mit einer throws-Klausel noch etwas deutlicher zu machen stellen wir uns vor, dass die funktionale Schnittstelle Predicate ein throws Exception (vom Sinn der Typs Exception an sich einmal abgesehen) enthält:
interface Predicate<T> { boolean test( T t ) throws Exception; } // Was wäre wenn?
Die Konsequenz wäre, dass jeder Aurufer von test(…) nun seinerseits die Exception in die Hände bekommt und sie auffangen oder weiterleiten muss. Leitet der test(….)-Aufrufer mit throws Exception die Ausnahme weiter nach oben, bekommen wir plötzlich an allen Stellen ein throws Exception in die Methodensignatur, was auf keinen Fall gewünscht ist. So enthält jetzt etwa ArrayList eine Deklaration von removeIf(Predicate filter); hier müsste dann removeIf(…) – was letztendlich filter.test(…) aufruft – sich mit der Test-Ausnahme rumärgern und removeIf(Predicate filter) throws Exception ist keine gute Sache.
Von geprüft nach ungeprüft
Geprüfte Ausnahmen sind in Lamba-Ausdrücken nicht schön. Eine Lösung ist, Code, der geprüfte Ausnahmen auslöst, zu verpacken und die geprüfte Ausnahme in einer ungeprüften zu manteln. Das kann etwa so aussehen:
public class PredicateWithException { @FunctionalInterface public interface ExceptionalPredicate<T, E extends Exception> { boolean test( T t ) throws E; } public static <T> Predicate<T> asUncheckedPredicate( ExceptionalPredicate<T, Exception> predicate ) { return t -> { try { return predicate.test( t ); } catch ( Exception e ) { throw new RuntimeException( e.getMessage(), e ); } }; } public static void main( String[] args ) { Predicate<Path> isEmptyFile = asUncheckedPredicate( path -> Files.size( path ) == 0 ); System.out.println( isEmptyFile.test( Paths.get( "c:/" ) ) ); } }
Die Schnittstelle ExceptionalPredicate ist ein Prädikat mit optionaler Ausnahme. In der eigenen Hilfsmethode asUncheckedPredicate(ExceptionalPredicate) nehmen wir so ein ExceptionalPredicate an und packen es in ein Predicate, was die Methode zurückgibt. Geprüfte Ausnahmen werden in eine ungeprüfte Ausnahme vom Typ RuntimeException gesetzt. Somit muss Predicate keine geprüfte Ausnahme weiterleiten, was es ja laut Deklaration auch nicht kann.
[1] Von Callable gibt es zwar Nutzer, die mit Nebenläufigkeit (daher das Paket java.util.concurrent) in Zusammenhang stehen, aber keine weiteren Verwendungen in der Java-Bibliothek, von zwei Beispielen aus javax.tools abgesehen. Mit java.util.function.Supplier existiert eine entsprechende Alternative ohne throws-Klausel.