Um nun ein wenig anzusehen, wie es sich mit den Exceptions verhält, gehen wir im Folgenden von dem nachfolgend gelisteten Service-Interface aus. Es ist ein FunctionalInterface
, jedoch leider mit der Definition einer Exception in der Methodensignatur:
public static interface Service {
String doWork(String txt) throws Exception;}
Wenn wir diese Exception nun verwenden wollen, dann kommen wir im klassischen Sinne zu einer einfachen Implementierung (die Implementierung selbst hat definitiv keinen tieferen Sinn):
try { new Service() { @Override public String doWork(String txt) throws Exception { return txt.toUpperCase() + "-workedOn"; } }.doWork(""); } catch (Exception e) { e.printStackTrace();
Wir können hier sehr schön den try-catch-Block erkennen, der dazu führt, dass wir uns überlegen müssen, wie wir mit einer Exception umgehen möchten. Soll die Exception einfach weiter durchgereicht werden? Da es sich um ein FunctionalInterface
handelt, können wir es natürlich auch als Lambda schreiben:
try { ((Service) txt -> txt.toUpperCase() + "-workedOn").doWork(""); } catch (Exception e) { e.printStackTrace();
Wenn man dieses Lambda-Konstrukt auf Klassenebene definiert, dann sieht es erst einmal recht einfach aus:
public static Service serviceA = txt -> txt.toUpperCase() + "-workedOnA"; public static Service serviceB = txt -> txt.toUpperCase() + "-workedOnB";
Nur leider kommen bei der Verwendung dann wieder die try-catch- Blöcke zum Vorschein:
try { final String helloA = serviceA.doWork("Hello A"); } catch (Exception e) { e.printStackTrace();
try { final String helloB = serviceB.doWork("Hello B"); } catch (Exception e) { e.printStackTrace();
Was aber ist nun eigentlich mit dem Rückgabewert? Darüber haben wir noch nicht nachgedacht, da gibt es nun zwei Wege. Der eine bedeutet, dass man die restliche Logik mit in den try-catch-Block schreibt. Kann man so machen, unhandlich wird es nur, wenn weitere Methoden mit Exceptions verwendet werden. Beginnt man nun die Blöcke ineinander zu verschachteln? Oder gibt es einen großen catch-Block am Ende aller Anweisungen? Alles recht unschön.
Der zweite Weg besteht darin, den try-catch-Block so kurz wie möglich zu halten. Der Ergebniswert wird dann in einer Variablen gespeichert, die vor dem try-catch-Block definiert wurde. Hier habe ich mich dafür entschieden, gleich mit einem Optional<T> zu arbeiten, da es im JDK auf jeden Fall vorhanden ist:
Optional<String> optional; try { final String result = ((Service) txt -> txt.toUpperCase() + "-workedOn").doWork(""); optional = Optional.of(result); } catch (Exception e) { e.printStackTrace(); optional = Optional.empty();
optional.ifPresent((result)-> System.out.println( "result = " + result));
Wie wäre es, wenn man nun diesen try-catch-Block ganz wegbekommen würde? Immerhin ist es immer dasselbe Stück Quelltext.
Um diesem Ziel näher zu kommen, definieren wir uns erst einmal eine Funktion, die mit solchen Methodensignaturen umgehen kann, die eine Exception definieren. Damit es wieder ein FunctionalInterface
wird, müssen wir uns überlegen, wie wir den Aufruf der Methode mit der definierten Exception
ummanteln können.
Ebenfalls gehe ich hier davon aus, dass ein möglicher Ergebniswert in einem Optional verpackt ausgeliefert werden wird. Damit erhalten wir als Erstes eine Funktion von T
auf Optional<T>
. Da der Eingangstyp nicht gleich dem Ausgangstyp sein muss, definieren wir es ein wenig allgemeiner: CheckedFunction<T, R> extends Function<T, Optional<R>>
.
Wir fügen nun eine Methode ein, die ebenfalls eine Exception in der Signatur definiert hat: R applyWithException(T t) throws Exception;
. Der Ablauf in beiden Fällen, also mit und ohne Auftreten einer Exception
, ist klar definiert. Diese Implementierung kann man nun als default
-Implementierung für die Methodensignatur default Optional<R> apply(T t)
nehmen:
@FunctionalInterfacepublic interface CheckedFunction<T, R> extends Function<T, Optional<R>> { @Override default Optional<R> apply(T t) { try { return Optional.ofNullable(applyWithException(t)); } catch (Exception e) { return Optional.empty(); } } R applyWithException(T t) throws Exception;
Eine klassische Implementierung von diesem Interface sieht dann wie folgt aus, wenn wir die vorherige Implementierung von dem Service-Interface als Grundlage nehmen:
final CheckedFunction<String, String> checkedFunction = new CheckedFunction<String, String>() { @Override public String applyWithException(String s) throws Exception { return ((Service) txt -> txt.toUpperCase() + "-workedOn").doWork(s); } };
Wenn nun diese CheckedFunction
verwendet wird, so sieht man noch die selbst definierte Methodensignatur: applyWithException
. Um dieses nun wieder loszuwerden, und damit die IDE nur noch das gewohnte apply anbietet, kann man es wieder auf eine Funktion casten:
final Function<String, Optional<String>> f = checkedFunction;
Die Verwendung kann nun auf die jeweiligen Fälle Bezug nehmen, da wir wie gewohnt mit einem Optional arbeiten:
f.apply("Hello") .ifPresent((result) -> System.out.println("result = " + result));
Was hier allerdings noch fehlt, ist der Zugriff auf die Exception beziehungsweise die Fehlermeldung, die im Fehlerfall geliefert worden ist. Hierzu modifizieren wir nun die CheckedFunction so, dass wir nicht das Optional<T>
verwenden, sondern die Klasse Result<T>
.
Die Klasse Result<T>
ist eine Erweiterung der Klasse Optional<T>
. Da Optional<T>
als final deklariert worden ist, ist die Klasse Result<T>
nicht in der Vererbungskette, sondern komplett unabhängig davon. Das besondere, was ich hier einsetzen möchte, ist die Möglichkeit, auch bei nicht vorhandenen Werten eine beschreibende Information zu halten. Wer hier Genaueres erfahren möchte, kann schon mal auf meinem Youtube-Kanal vorbeischauen ( http://bit.ly/ FRP-Result-DE):
@FunctionalInterface public interface CheckedFunction<T, R> extends Function<T, Result<R>> { @Override default Result<R> apply(T t) { try { return Result.success(applyWithException(t)); } catch (Exception e) { final String message = e.getMessage(); return Result.failure((message != null) ? message : e.getClass().getSimpleName()); } } R applyWithException(T t) throws Exception;
In der Verwendung kann man nun auf die erweiterten Möglichkeiten, die uns das Result liefert, zugreifen:
final Consumer<String> print = System.out::println; final Function<String, Result<String>> checkedFunction = (CheckedFunction<String, String>) ((Service) txt -> txt.toUpperCase() + "-workedOn")::doWork;
checkedFunction.apply("Hello") .ifPresentOrElse( (result) -> print.accept("result = " + result), (failed) -> print.accept("failed = " + failed) );
In dem Open-Source-Projekt Functional-Reactive auf GitHub ( https://github.com/svenruppert/functional-reactive-lib) s ind die Checked-Functions
zu finden. Ebenfalls gibt es dort auch CheckedConsumer
, CheckedSupplier
, CheckedBiFunction
, ...
Cheers Sven