Optional Einführung
Die Null-Pointer-Exception ist eine Qual im Gesäß und manche Entwickler führen wahre Kreuzzüge gegen das null
-Literal.
Die Optionals, die mit Java 8 eingeführt wurden, beseitigen das Problem nicht rückstandsfrei, helfen aber – bei
Erhaltung der Lesbarkeit – die Widerstandsfähigkeit des Codes zu erhöhen.
Wer sich mit den Optionals schon einigermaßen wohlfühlt, findet auf dieser DZone-Card eine gute Zusammenstellung von Regeln für den täglichen Gebrauch.
Was ist ein Optional?
Das grundlegende Problem des null
-Wertes ist, daß er sich nicht wie ein Objekt verhält. Jeder Methoden-
oder Feld-Aufruf daran bricht und verdampft als Exception. Die Java-Lösung ist, den -- möglicherweise null-seienden
Wert in ein Objekt zu verpacken, das garantiert ein solches ist -- das Optional
-Objekt.
Optional
ist eine generische Klasse des JDK, dazu erdacht Objekte einer anderen Klasse zu umhüllen.
Man darf sich Optionals allerdings nicht als Daten-Container vorstellen, der der dauerhaften Speicherung dient.
Optionals sind Helfer bei der Verarbeitung und Manipulation von Objekten.
Eine etwas ausführlichere Begründung findet sich [hier|| nicht serialisierbar].
der Einfachheit halber betrachten wir im Folgenden ausschließlich Optionals die Strings einwickeln. Anstelle von Strings kann natürlich jede beliebige Java-Klasse verwendet werden; wobei darauf hingeweisen sei, daß λ-Ausdrücke keine Klassen-Instanzen sind und nicht ver-optionalt werden können.
Optionals erzeugen
Um einen Wert in ein Optional
-Objekt zu verpacken muß man eines erzeugen.
Man kann das mit Hilfe dreier statischer Methoden erreichen:
of: nur mit garantiertem Inhalt
Ist garantiert, daß der einzupackende String nicht null
ist, kann man Optional.of()
verwenden:
Optional<String> eins = Optional.of("eins"); String foo = "foo"; Optional<String> zwei = Optional.of(foo.trim());
Aber: Ruft man of
mit null
auf erhält man eine Exception.
Warum sollte man einen garantiert nicht-nullen Wert in ein Optional verpacken wollen? Antwort: Der Uniformität wegen. Wird im weiteren Verlauf ein Optional verlangt, kann man ein solches liefern. Soll eine Methode ein Optional liefern, kann man zum Beispiel Literale ohne Overhead in Optionals verpacken.
ofNullable: geht immer
Kann man nicht sicher sein, daß das Argument nicht null
ist, nutzt man die statische Methode Optional.ofNullable()
.
Die Anwendung entspricht der von of()
, nur daß null
als Argument diesmal explizit erlaubt ist und keine Exception wirft:
Optional<String> drei = Optional.ofNullable("drei"); Optional<String> vier = Optional.ofNullable(null); String foo2 = null; Optional<String> fuenf = Optional.ofNullable(foo2);
empty: nie was drin
Die parameterlose Methode Optional.empty()
verwendet man, wenn man null
einpacken möchte.
Ihre Verwendung entspricht Optional.ofNullable(null)
:
Optional<String> immerNull = Optional.empty();
Auf die Frage nach der Sinnhaftigkeit läßt sich die gleiche Antwort geben wir vorhin...
Default-Werte mit Optionals
Haben wir einen Wert verpackt, möchten wir ihn auch verarbeiten.
Das erste ist die Prüfung, ob das Optional einen Wert ungleich null
enthält. Das geschieht
mit der Methode isPresent()
. Sodann möchten wir auf den Inhalt zugreifen, wozu die Methode get()
dient.
Das geht allerdings nur, wenn der Wert ungleich null
ist, andernfalls giebt's eine Exception. Im folgenden Beispiel
prüfen wir und liefern dann den Wert oder -- im null
-Falle -- den default-Wert "nichts".
Optional<String> zahl = Optional.of("sechs"); if (sechs.isPresent()) { return zahl.get(); } else { return "nichts"; }
Damit wäre das – bei Clean-Codern so verhaßte – null
-Literal eliminiert, aber wirklich hilfreich ist das eigentlich nicht
denn besonders hübsch sieht's auch nicht aus. Eleganter wird die Sache, wenn man den Default-Wert mit der Methode orElse()
liefert.
Dann schnurrt obiges Code-Fragment so zusammen:
Optional<String> zahl = Optional.of("sechs"); return zahl.orElse("nichts");
und ohne die lokale Variable:
return Optional.of("sechs").orElse("nichts");
Das ist doch nun wirklich kompakt. Für die Literale "sechs"
und "nichts"
kann man natürlich einsetzen,
was einem beliebt. Da ich Optionals sehr oft verwende, importiere ich ofNullable
bzw. of
gerne statisch,
dann fällt sogar das qualifizierende "Optional" weg.
Für die folgenden Features wird ein grundsätzliches Verständnis von Lambda-Ausdrücken benötigt.
Eine kurze Einführung giebt's hier.
Nur ausführen, wenn nicht null
Möchte man auf ein Objekt, das man aus dem Optional gepellt hat, eine Methode anwenden kann man das mit ifPresent()
tun:
void use(String x) { // ... } ... Optional<String> me = ofNullable("Peter"); return me.ifPresent(this::use);
Die Methode use()
wird nur dann mit dem Inhalt von me
ausgeführt, wenn dieser nicht null
ist.
Leider giebt es für diesen Fall in Java 8 keine Möglichkeit dem Optional zu sagen was es tun soll, wenn der Inhalt null
ist.
In Java 11 giebt es dafür die Methode ifPresentOrElse
die auch diesen Fall abdeckt.
Ab hier nur mit Lambdas
Ausführen und weitermachen
Mit Hilfe der Methode map()
kann man aus einem Optional eine regelrechte Verarbeitungs-Pipeline machen.
Der typische, oft gesehene - und ausgesprochen häßliche - Anwendungsfall hierfür sieht zum Beispiel so aus:
Kiste x = new Kiste(); if (x != null) { Schachtel s = x.getSchachtel(); if (s != null) { Flasche f = s.getFlasche(); if (f != null) { return f; } } } return Flasche.EMPTY;
Es gibt verschiedene Möglichkeiten, solche if-Kaskaden in Java zu programmieren – eine schlimmer als die andere.
Mit Optional
und map()
sieht das so aus:
Kiste x = new Kiste(); return Optional.ofNullable(x).map(Kiste::getSchachtel).map(Schachtel::getFlasche).orElse(Flasche.EMPTY);
Die Lesbarkeit läßt sich deutlich erhöhen indem man den Ausdruck auf mehrere Zeilen verteilt. Die Kommentare am Zeilenende erzwingen den Umbruch und sorgen für die Einrückung:
Kiste x = new Kiste(); return Optional.ofNullable(x) \\ .map(Kiste::getSchachtel) \\ .map(Schachtel::getFlasche) \\ .orElse(Flasche.EMPTY);
Die Methode map()
wendet den – als Parameter übergebenen – λ-Ausdruck auf den Wert an, der im Optional verpackt ist. Das aber nur, wenn er nicht null
ist. Das Ergebnis wird in ein Optional verpackt und weitergegeben. Das bedeutet, daß sich durch jede Anwendung von map()
der Typ des Optionals ändert. Zerlegen wir mal das Beispiel:
liefert ein Objekt vom Typ | |
---|---|
Optional.ofNullable(x)
|
Optional<Kiste>
|
.map(Kiste::getSchachtel)
|
Optional<Schachtel>
|
.map(Schachtel::getFlasche)
|
Optional<Flasche>
|
.orElse(Flasche.EMPTY)
|
Flasche
|
Zum Glück verfolgt Eclipse ganz genau, wann welches Objekt welchen Typ haben kann oder muß, wir bekommen also jederzeit Hinweise wenn was nicht stimmt oder was wir als nächstes verwenden können.
Default-Werte nur bei Bedarf berechnen
Hat man einen Default-Wert, dessen Berechnung teuer ist – zum Beispiel einen Netzwerk- oder Datenbank-Zugriff benötigt -
oder ein riesiges Objekt erzeugen würde kann man die Berechnung verzögern und den Wert nur dann berechnen, wenn er tatsächlich
gebraucht wird. Das geschieht mit orElseGet()
:
String teurerWert() { return Datenbank.holeErgebnis().extrahiereString(); } ... Optional<String> eingabe = Optional.ofNullable(getUserEingabe()); return eingabe.orElseGet(this::teurerWert);
Speziell für das Werfen von Exceptions gibt es eine eigene Methode:
return eingabe.orElseThrow((x) -> new IllegalArgumentException("giebt's nich")));
Wenn der Konstruktor der Exception keine Parameter hat, kann man für den Aufruf auch die ::
-Form verwenden.
return eingabe.orElseThrow(IllegalArgumentException::new);