Ausnahmen in Java 8 Lambda-Ausdrücken

1. Übersicht

In Java 8 begann Lambda Expressions, die funktionale Programmierung zu vereinfachen, indem eine präzise Möglichkeit zum Ausdrücken von Verhalten bereitgestellt wurde. Die vom JDK bereitgestellten funktionalen Schnittstellen behandeln Ausnahmen jedoch nicht sehr gut - und der Code wird ausführlich und umständlich, wenn es darum geht, sie zu behandeln.

In diesem Artikel werden einige Möglichkeiten zum Umgang mit Ausnahmen beim Schreiben von Lambda-Ausdrücken untersucht.

2. Umgang mit ungeprüften Ausnahmen

Lassen Sie uns zunächst das Problem anhand eines Beispiels verstehen.

Wir haben eine Liste und möchten eine Konstante, z. B. 50, mit jedem Element dieser Liste teilen und die Ergebnisse ausdrucken:

List integers = Arrays.asList(3, 9, 7, 6, 10, 20); integers.forEach(i -> System.out.println(50 / i));

Dieser Ausdruck funktioniert, aber es gibt ein Problem. Wenn eines der Elemente in der Liste 0 ist , erhalten wir eine ArithmeticException: / by zero . Beheben wir das, indem wir einen herkömmlichen Try-Catch- Block verwenden, sodass wir eine solche Ausnahme protokollieren und die Ausführung für die nächsten Elemente fortsetzen:

List integers = Arrays.asList(3, 9, 7, 0, 10, 20); integers.forEach(i -> { try { System.out.println(50 / i); } catch (ArithmeticException e) { System.err.println( "Arithmetic Exception occured : " + e.getMessage()); } });

Die Verwendung von Try-Catch löst das Problem, aber die Prägnanz eines Lambda-Ausdrucks geht verloren und es ist keine kleine Funktion mehr, wie sie sein soll.

Um dieses Problem zu lösen, können wir einen Lambda-Wrapper für die Lambda-Funktion schreiben . Schauen wir uns den Code an, um zu sehen, wie er funktioniert:

static Consumer lambdaWrapper(Consumer consumer) { return i -> { try { consumer.accept(i); } catch (ArithmeticException e) { System.err.println( "Arithmetic Exception occured : " + e.getMessage()); } }; }
List integers = Arrays.asList(3, 9, 7, 0, 10, 20); integers.forEach(lambdaWrapper(i -> System.out.println(50 / i)));

Zuerst haben wir eine Wrapper-Methode geschrieben, die für die Behandlung der Ausnahme verantwortlich ist, und dann den Lambda-Ausdruck als Parameter an diese Methode übergeben.

Die Wrapper-Methode funktioniert wie erwartet, aber Sie können argumentieren, dass sie im Grunde genommen den try-catch- Block aus dem Lambda-Ausdruck entfernt und in eine andere Methode verschiebt und die tatsächliche Anzahl der geschriebenen Codezeilen nicht verringert.

Dies gilt in diesem Fall, in dem der Wrapper für einen bestimmten Anwendungsfall spezifisch ist. Wir können jedoch Generika verwenden, um diese Methode zu verbessern und sie für eine Vielzahl anderer Szenarien zu verwenden:

static  Consumer consumerWrapper(Consumer consumer, Class clazz) { return i -> { try { consumer.accept(i); } catch (Exception ex) { try { E exCast = clazz.cast(ex); System.err.println( "Exception occured : " + exCast.getMessage()); } catch (ClassCastException ccEx) { throw ex; } } }; }
List integers = Arrays.asList(3, 9, 7, 0, 10, 20); integers.forEach( consumerWrapper( i -> System.out.println(50 / i), ArithmeticException.class));

Wie wir sehen können, benötigt diese Iteration unserer Wrapper-Methode zwei Argumente, den Lambda-Ausdruck und den Typ der Ausnahme , die abgefangen werden soll. Dieser Lambda-Wrapper kann alle Datentypen verarbeiten, nicht nur Ganzzahlen , und einen bestimmten Ausnahmetyp abfangen, nicht die Ausnahme der Oberklasse .

Beachten Sie außerdem, dass wir den Namen der Methode von lambdaWrapper in consumerWrapper geändert haben . Dies liegt daran, dass diese Methode nur Lambda-Ausdrücke für die Funktionsschnittstelle vom Typ Consumer verarbeitet . Wir können ähnliche Wrapper-Methoden für andere funktionale Schnittstellen wie Function , BiFunction , BiConsumer usw. schreiben .

3. Umgang mit geprüften Ausnahmen

Lassen Sie uns das Beispiel aus dem vorherigen Abschnitt ändern und statt auf der Konsole zu drucken, in eine Datei schreiben.

static void writeToFile(Integer integer) throws IOException { // logic to write to file which throws IOException }

Beachten Sie, dass die obige Methode möglicherweise die IOException auslöst.

List integers = Arrays.asList(3, 9, 7, 0, 10, 20); integers.forEach(i -> writeToFile(i));

Beim Kompilieren erhalten wir den Fehler:

java.lang.Error: Unresolved compilation problem: Unhandled exception type IOException

Da IOException eine aktivierte Ausnahme ist, müssen wir sie explizit behandeln . Wir haben zwei Möglichkeiten.

Erstens können wir die Ausnahme einfach außerhalb unserer Methode werfen und sie woanders erledigen.

Alternativ können wir es innerhalb der Methode behandeln, die einen Lambda-Ausdruck verwendet.

Lassen Sie uns beide Optionen untersuchen.

3.1. Aktivierte Ausnahme von Lambda-Ausdrücken auslösen

Mal sehen, was passiert, wenn wir die IOException für die Hauptmethode deklarieren :

public static void main(String[] args) throws IOException { List integers = Arrays.asList(3, 9, 7, 0, 10, 20); integers.forEach(i -> writeToFile(i)); }

Trotzdem erhalten wir während der Kompilierung den gleichen Fehler wie bei einer nicht behandelten IOException .

java.lang.Error: Unresolved compilation problem: Unhandled exception type IOException

Dies liegt daran, dass Lambda-Ausdrücke anonymen inneren Klassen ähnlich sind.

In unserem Fall ist die writeToFile- Methode die Implementierung der Consumer- Funktionsschnittstelle .

Werfen wir einen Blick auf die Definition des Verbrauchers :

@FunctionalInterface public interface Consumer { void accept(T t); }

Wie wir sehen können , deklariert die Methode accept keine aktivierte Ausnahme. Aus diesem Grund writeToFile ist nicht zu werfen erlaubt IOException.

Am einfachsten wäre es, einen Try-Catch- Block zu verwenden, die aktivierte Ausnahme in eine nicht aktivierte Ausnahme zu verpacken und erneut zu werfen:

List integers = Arrays.asList(3, 9, 7, 0, 10, 20); integers.forEach(i -> { try { writeToFile(i); } catch (IOException e) { throw new RuntimeException(e); } }); 

Dadurch wird der Code kompiliert und ausgeführt. Dieser Ansatz führt jedoch das gleiche Problem ein, das wir bereits im vorherigen Abschnitt erörtert haben - es ist ausführlich und umständlich.

Wir können besser werden.

Erstellen wir eine benutzerdefinierte Funktionsschnittstelle mit einer einzelnen Akzeptanzmethode , die eine Ausnahme auslöst.

@FunctionalInterface public interface ThrowingConsumer { void accept(T t) throws E; }

Und jetzt implementieren wir eine Wrapper-Methode, mit der die Ausnahme erneut ausgelöst werden kann:

static  Consumer throwingConsumerWrapper( ThrowingConsumer throwingConsumer) { return i -> { try { throwingConsumer.accept(i); } catch (Exception ex) { throw new RuntimeException(ex); } }; }

Schließlich können wir die Verwendung der writeToFile- Methode vereinfachen :

List integers = Arrays.asList(3, 9, 7, 0, 10, 20); integers.forEach(throwingConsumerWrapper(i -> writeToFile(i)));

Dies ist immer noch eine Art Problemumgehung, aber das Endergebnis sieht ziemlich sauber aus und ist definitiv einfacher zu warten .

Sowohl der ThrowingConsumer als auch der throwingConsumerWrapper sind generisch und können problemlos an verschiedenen Stellen unserer Anwendung wiederverwendet werden.

3.2. Handling a Checked Exception in Lambda Expression

In this final section, we'll modify the wrapper to handle checked exceptions.

Since our ThrowingConsumer interface uses generics, we can easily handle any specific exception.

static  Consumer handlingConsumerWrapper( ThrowingConsumer throwingConsumer, Class exceptionClass) { return i -> { try { throwingConsumer.accept(i); } catch (Exception ex) { try { E exCast = exceptionClass.cast(ex); System.err.println( "Exception occured : " + exCast.getMessage()); } catch (ClassCastException ccEx) { throw new RuntimeException(ex); } } }; }

Let's see how to use it in practice:

List integers = Arrays.asList(3, 9, 7, 0, 10, 20); integers.forEach(handlingConsumerWrapper( i -> writeToFile(i), IOException.class));

Note, that the above code handles only IOException, whereas any other kind of exception is rethrown as a RuntimeException .

4. Conclusion

In this article, we showed how to handle a specific exception in lambda expression without losing the conciseness with the help of wrapper methods. We also learned how to write throwing alternatives for the Functional Interfaces present in JDK to either throw or handle a checked exception.

Another way would be to explore the sneaky-throws hack.

Der vollständige Quellcode der Functional Interface- und Wrapper-Methoden kann von hier heruntergeladen und die Klassen von hier auf Github getestet werden.

Wenn Sie nach sofort einsatzbereiten Lösungen suchen, sollten Sie sich das ThrowingFunction-Projekt ansehen.