22. Februar 2018 von Ramon Küpfer
Java-Optionals
Codebase Migration
Um ein Beispiel heranzuziehen, gehe ich im Folgenden von einem Projekt aus, dessen Code-Basis bereits seit einigen Jahren entwickelt wird. Die Entwickler in diesem Projekt haben beispielsweise erste Erfahrungen mit Guava gemacht und einige Optionals dieser Bibliothek wurden auch schon in Klassen eingebaut. Zudem basiert unser Beispielprojekt auf einem Servicelayer, der Optionals in seinen Schnittstellen anbietet und erwartet. So weit, so gut. Bald stellt ihr allerdings fest, dass es eine Klasse gibt, die nicht nur Guava-, sondern auch Java-Optionals enthält. Diese Klasse könnte, wenn auch stark vereinfacht, folgendermaßen aussehen:
import com.google.common.base.Optional;
...
public class MixedStyleClass {
private Optional<Device> device;
private java.util.Optional<Configuration> configuration;
public Optional<Device> getDevice() { … }
public void setConfiguration(java.util.Optional<Configuration> config) { … }
}
Redundante Verwendung einer zweiten Optional Klasse
Bei einigen von euch werden sich sicherlich die Nackenhaare aufstellen, wenn es um die Verwendung beider Klassen von Optionals geht. Hättet ihr den Code nicht um ein Optional reduzieren können? Das ist jedoch einfacher gesagt als getan und zwar aus verschiedenen Gründen:
- Die Änderung einer Methodensignatur und das Ersetzen einer Klasse in einer Schnittstelle können größere Anpassungen in den Konsumenten dieser Schnittstelle nach sich ziehen.
- Nicht alle Importe können auf eine einfache Art und Weise von Guava auf Java geändert werden, denn die beiden Klassen unterscheiden sich in ihren Methoden und der Code müsste entsprechend angepasst werden.
- Die Schnittstellen im Servicelayer sind mit Abhängigkeiten zu anderen Projekten nicht ohne weiteres änderbar.
- Häufig fehlt den Entwicklern schlicht und ergreifend die Zeit zur Umsetzung.
Ich gehe in meinem Projektbeispiel davon aus, dass die Methoden im Servicelayer nicht änderbar sind und dass die Änderungen für den Anfang möglichst klein und begrenzt sein sollen. Ihr als Entwickler profitiert von eurer Erfahrung, wenn es darum geht, zu unterscheiden, ob ihr in der verfügbaren Zeit nur die Optionals dieser einen Klasse ersetzt oder ob ihr weitere Konsumenten dieser Klasse in den Umbau mit einbeziehen wollt. Spätestens beim Aufruf des Servicelayers werdet ihr allerdings beide Optionals entsprechend mappen müssen. Um dieses Mapping nicht immer wieder neu zu schreiben, benötigt ihr entsprechende Methoden, um Optionals in beide Richtungen zu konvertieren. Ein Beispiel seht ihr in folgender Hilfsklasse:
public class Optionals {
public static Optional fromGuavaToJava(com.google.common.base.Optional optional) {
return optional.transform(Optional::of)
.or(Optional.empty());
}
public static com.google.common.base.Optional fromJavaToGuava(Optional optional) {
return optional.map(com.google.common.base.Optional::of)
.orElse(com.google.common.base.Optional.absent());
}
}
Hilfsfunktionen zur Konvertierung der unterschiedlichen Optional Klassen
Aufzurufende Methoden im Servicelayer, die ein Guava-Optional zurückliefern, werden mittels Optionals.fromGuavaToJava(…)
transformiert. Entsprechend liefert Optionals.fromJavaToGuava(…)
das Guava-Optional, das vom Konsumenten erwartet wird. Mit diesem Werkzeug könnt ihr nun Optionals an einer Schnittstelle transformieren.
Anti-Patterns mit Optionals
Nachdem also nun geklärt ist, wie Optionals an den Schnittstellen behandelt werden, bleiben noch Korrekturen im Code, da nicht beide Klassen von Optionals dieselben Methoden anbieten. Während Guava „Optionals transform“ anbietet, heißt die Methode in Java „map“. Hier wird beispielsweise fromNullable
zu ofNullabl
oder absent
zu empty
. Ein Vorteil ist, dass nur wenige Methoden über identische Namen – wie beispielsweise get
und isPresent
– verfügen.
In diesem Schritt habt ihr die Möglichkeit, gewisse Anti-Patterns von Optionals zu korrigieren – sie werden nämlich leider häufig als besserer Null-Check eingesetzt. Der Code wird damit aber nicht unbedingt leserlicher.
public Optional<Integer> getOrderNumber(Optional<Order> previousOrder) {
if (previousOrder.isPresent()) {
return Optional.fromNullable(previousOrder.get().getOrderNumber());
}
return Optional.absent();
}
Unnötige Verwendung von isPresent() und get()
Das ist ein gutes Beispiel, um euch zu zeigen, wie ein Optional den Code unnötigerweise aufbläht. Die Verwendung von isPresent
ist unnötig verbal. Anstatt umständlich fromNullable
und absent
einzusetzen, benutzt nach der Konvertierung lieber map
:
public Optional<Integer> getOrderNumber(Optional<Order> previousOrder) {
return previousOrder.map(Order::getOrderNumber);
}
Anwendung von map()
Mittels map
werden übergebene Methoden auf dem Wert des Optionals ausgeführt − sofern er vorhanden ist. Falls nicht, wird ein empty
zurückgeliefert. Als Funktionsargument übergebt ihr den gewünschten Getter der Klasse. Auf diese Weise wird der Wert in einem Optional zurückgeliefert. Damit wird auch return
überflüssig, vier Zeilen Code reduzieren sich auf eine und der Code wird viel leserlicher:Optional.absent()
if (previousOrder.isPresent()) {
final Order order = previousOrder.get();
initAddressesFromPreviousOrder(model, order);
}
Umständliche Verwendung unnötiger Methoden
Der Aufruf von isPresent
ist auch hier überflüssig und mit ifPresent
habt ihr die Möglichkeit, direkt eine Methode aufzurufen. Wenn previousOrder
empty
ist, wird kein Code ausgeführt. Somit erhaltet ihr:
previousOrder.ifPresent(order → initAddressesFromPreviousOrder(model, order));
Anwendung von ifPresent()
Neuerung für Optional in Java 9 – die Methode or
Die Neuerungen in Java 9 bieten euch nochmals zusätzliche Methoden auf der Optional-Klasse. Verwendet ihr einen Optional, hattet ihr bisher mit orElse
oder orElseGet
die Möglichkeit, einen Standardwert zurückzuliefern, falls Optional empty
war. Allerdings habt ihr den Context eines Optionals verloren und musstet das Resultat − falls ein weiterer Optional benötigt wurde − wieder mittels Optional.of
kapseln. Dies gehört mit der Methode or(Supplier<? Extends Optional<? Extends T>> supplier)
, die ein Optional<T>
zurückliefert, der Vergangenheit an. Wenn ihr euch weiter mit diesem Thema beschäftigen möchtet, empfehle ich euch den Beitrag im Java Magazin, der auch noch auf ifPresentOrElse
und stream
eingeht.
Wie ihr gesehen habt, lohnt sich ein Blick auf die angebotenen Methoden der Java-Optional-Klasse − auch im Hinblick auf die Neuerungen, die mit Java 9 kommen werden.