Ausnahmen in Lambda-Ausdrücken

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.

Ähnliche Beiträge

3 Gedanken zu “Ausnahmen in Lambda-Ausdrücken

  1. Meine Erfahrungen mit Checked Exceptions und Java8 sind inzwischen dass es am besten ist erstere gezielt zu deaktivieren wenn sie die Lambdas stören. Dazu verwende ich in einer Sammlung statischer Methoden „sneakyThrow(ExceptionalConsumer/ExceptionalRunnable/ExceptionalPredicate)“ dasselbe Pattern wie du hier beschreibst. Jedoch leite ich die Exception direkt durch anstatt sie in unchecked exceptions umzukapseln. Dazu verwende ich den alten Trick mit Generics:

    http://james-iry.blogspot.de/2010/08/on-removing-java-checked-exceptions-by.html

    Außerhalb des Streams/forEach deklariere ich die Exception dann wieder als throws um „sauber“ zu bleiben. Das finde ich sehr viel einfacher als die Exception ständig mit „Unchecked(IO/Execution/XML/CloneNotSupported;-))-Exception ein- und wieder auszupacken.

    Lombok’s SneakyThrows ist auch eine Alternative, jedoch nicht in allen Projekten erlaubt. Für die Zukunft hoffe ich das Java selbst Annotationen einführt mit denen sich die Checked Exceptions a la Lombok deaktivieren kann, denn das derzeitige Verhalten und Lambda Expressions passen einfach nicht zusammen.

    @SupressWarnings und @SafeVarargs machen ja auch nichts anderes als das Typsystem zu beschwichtigen. Ich verstehe deshalb nicht warum man gerade an den Checked Exceptions weiterhin wie an einem Mantra festhält.

  2. Hallo Christian,

    Danke für Deinen Bericht! Der ist sehr hilfreich.

    Jedoch kann ich dem Statement „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“ nicht so ganz zustimmen.

    Es ist doch in Java gar kein Problem, die meisten der selbst erstellten Methoden mit „… throws Exception“ abzuschließen – das ist mir viel vertrauter als das Gegenteil. Denn die meisten Programm hängen doch weitestgehend von der „externen“ Input/Output-Welt ab, wo jederzeit Exceptions auftreten können (Datenbank, Serververbindungen, etc.). Und je komplexer Software wird, desto mehr wird sie von Funktionen abhängen, die wiederum Exceptions auslösen können.

    „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. “

    Warum soll das nicht gewünscht sein? Ich sehe darin überhaupt keine Nachteile. Schließlich macht es Exception Handling über den ganzen Call-Stack hin transparent und nachvollziehbar.

    Ich würde viel lieber empfehlen, eigen FunctionalInterfaces zu stricken, die jeweils z.B. eine appy(…) Methode haben, die mit „throws Exception“ abschließt. Man kann den Exceptiontype falls nötig ja auch generisch gestalten, für meine Fälle ist dies aber meistens nicht erforderlich.

    Freue mich natürlich auch über Deine Hinweise, vlt. habe ich einen speziellen Aspekt noch nicht bedacht.

    Schöne Grüße!

  3. Alles mit throws Exception zu setzen ist eine schlechte Idee, denn so wird dem Aufrufer ja überhaupt nicht klar, WAS genau passieren kann — Exception ist die Basisklasse aller Ausnahmen (lassen wir Throwable einmal außen vor). Wir wollen aber so präzise Angaben wir möglich, eine IOException ist etwas anderes als eine ArithmeticExcecption.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert