Optional
Häufig hat man in Methoden den Fall, dass es keinen Wert gibt, und man liefert dann
null
als "kein Wert vorhanden" zurück. Dies führt dazu, dass die Aufrufer eine
entsprechende null
-Prüfung für die Rückgabewerte durchführen müssen, bevor sie
das Ergebnis nutzen können.
Optional
schließt elegant den Fall "kein Wert vorhanden" ein: Es kann mit der Methode
Optional.ofNullable()
das Argument in ein Optional verpacken (Argument != null
)
oder ein Optional.empty()
zurückliefern ("leeres" Optional, wenn Argument == null
).
Man kann Optionals prüfen mit isEmpty()
und ifPresent()
und dann direkt mit
ifPresent()
, orElse()
und orElseThrow()
auf den verpackten Wert zugreifen.
Besser ist aber der Zugriff über die Stream-API von Optional
: map()
, filter
,
flatMap()
, ...
Optional
ist vor allem für Rückgabewerte gedacht, die den Fall "kein Wert vorhanden"
einschließen sollen. Attribute, Parameter und Sammlungen sollten nicht Optional
-Referenzen
speichern, sondern "richtige" (unverpackte) Werte (und eben zur Not null
). Optional
ist kein Ersatz für null
-Prüfung von Methoden-Parametern (nutzen Sie hier beispielsweise
passende Annotationen). Optional
ist auch kein Ersatz für vernünftiges Exception-Handling
im Fall, dass etwas Unerwartetes passiert ist. Liefern Sie niemals null
zurück, wenn
der Rückgabetyp der Methode ein Optional
ist!
- (K2) Optionals sind kein Ersatz für
null
-Prüfung! - (K2) Optionals sollen nicht für Attribute oder Parameter genutzt werden
- (K2) Es darf kein
null
zurückgeliefert werden, wenn der Rückgabetyp ein Optional ist - (K2) Optionals und
null
sind kein Ersatz für Exception-Handling - (K3) Einsatz von
Optional
in Rückgabewerten - (K3) Erzeugen von Optionals mit
Optional.ofNullable()
- (K3) Zugriff auf Optionals entweder direkt oder per Stream-API
Motivation
public class LSF {
private Set<Studi> sl;
public Studi getBestStudi() {
if (sl == null) return null; // Fehler: Es gibt noch keine Sammlung
Studi best = null;
for (Studi s : sl) {
if (best == null) best = s;
if (best.credits() < s.credits()) best = s;
}
return best;
}
}
public static void main(String... args) {
LSF lsf = new LSF();
Studi best = lsf.getBestStudi();
if (best != null) {
String name = best.name();
if (name != null) {
// mach was mit dem Namen ...
}
}
}
Problem: null
wird an (zu) vielen Stellen genutzt
- Es gibt keinen Wert ("not found")
- Felder wurden (noch) nicht initialisiert
- Es ist ein Problem oder etwas Unerwartetes aufgetreten
=> Parameter und Rückgabewerte müssen stets auf null
geprüft werden
(oder Annotationen wie @NotNull
eingesetzt werden ...)
Lösung
Optional<T>
für Rückgabewerte, die "kein Wert vorhanden" mit einschließen (stattnull
bei Abwesenheit von Werten)@NotNull
/@Nullable
für Parameter einsetzen (oder separate Prüfung)- Exceptions werfen in Fällen, wo ein Problem aufgetreten ist
Anmerkungen
- Verwendung von
null
auf Attribut-Ebene (Klassen-interne Verwendung) ist okay! Optional<T>
ist kein Ersatz fürnull
-Checks!null
ist kein Ersatz für vernünftiges Error-Handling! Das häufig zu beobachtende "Irgendwas Unerwartetes ist passiert, hier istnull
" ist ein Anti-Pattern!
Beispiel aus der Praxis im PM-Dungeon
Schauen Sie sich einmal das Review zu den ecs.components.ai.AITools
in
https://github.com/Dungeon-CampusMinden/Dungeon/pull/128#pullrequestreview-1254025874
an.
Die Methode AITools#calculateNewPath
soll in der Umgebung einer als Parameter
übergebenen Entität nach einem Feld (Tile
) suchen, welches für die Entität
betretbar ist und einen Pfad von der Position der Entität zu diesem Feld an den
Aufrufer zurückliefern.
Zunächst wird in der Entität nach einer PositionComponent
und einer VelocityComponent
gesucht. Wenn es (eine) diese(r) Components nicht in der Entität gibt, wird der Wert
null
an den Aufrufer von AITools#calculateNewPath
zurückgeliefert.
(Anmerkung: Interessanterweise wird in der Methode nicht mit der VelocityComponent
gearbeitet.)
Dann wird in der PositionComponent
die Position der Entität im aktuellen Level
abgerufen. In einer Schleife werden alle Felder im gegebenen Radius in eine Liste
gespeichert.
(Anmerkung: Da dies über die float
-Werte passiert und nicht über die Feld-Indizes
wird ein Tile
u.U. recht oft in der Liste abgelegt. Können Sie sich hier einfache
Verbesserungen überlegen?)
Da level.getTileAt()
offenbar als Antwort auch null
zurückliefern kann, werden
nun zunächst per tiles.removeIf(Objects::isNull);
all diese null
-Werte wieder aus
der Liste entfernt. Danach erfolgt die Prüfung, ob die verbleibenden Felder betretbar
sind und nicht-betretbare Felder werden entfernt.
Aus den verbleibenden (betretbaren) Feldern in der Liste wird nun eines zufällig
ausgewählt und per level.findPath()
ein Pfad von der Position der Entität zu diesem
Feld berechnet und zurückgeliefert.
(Anmerkung: Hier wird ein zufälliges Tile in der Liste der umgebenden Felder gewählt,
von diesem die Koordinaten bestimmt, und dann noch einmal aus dem Level das dazugehörige
Feld geholt - dabei hatte man die Referenz auf das Feld bereits in der Liste. Können
Sie sich hier eine einfache Verbesserung überlegen?)
Zusammengefasst:
- Die als Parameter
entity
übergebene Referenz darf offenbar nichtnull
sein. Die ersten beiden Statements in der Methode rufen auf dieser Referenz Methoden auf, was bei einernull
-Referenz zu einerNullPointer
-Exception führen würde. Hier wärenull
ein Fehlerzustand. entity.getComponent()
kann offenbarnull
zurückliefern, wenn die gesuchte Component nicht vorhanden ist. Hier wirdnull
als "kein Wert vorhanden" genutzt, was dann nachfolgendenull
-Checks notwendig macht.- Wenn es die gewünschten Components nicht gibt, wird dem Aufrufer der Methode
null
zurückgeliefert. Hier ist nicht ganz klar, ob das einfach nur "kein Wert vorhanden" ist oder eigentlich ein Fehlerzustand? level.getTileAt()
kann offenbarnull
zurückliefern, wenn kein Feld an der Position vorhanden ist. Hier wirdnull
wieder als "kein Wert vorhanden" genutzt, was dann nachfolgendenull
-Checks notwendig macht (Entfernen allernull
-Referenzen aus der Liste).level.findPath()
kann auch wiedernull
zurückliefern, wenn kein Pfad berechnet werden konnte. Hier ist wieder nicht ganz klar, ob das einfach nur "kein Wert vorhanden" ist oder eigentlich ein Fehlerzustand? Man könnte beispielsweise in diesem Fall ein anderes Feld probieren?
Der Aufrufer bekommt also eine NullPointer
-Exception, wenn der übergebene Parameter
entity
nicht vorhanden ist oder den Wert null
, wenn in der Methode etwas schief
lief oder schlicht kein Pfad berechnet werden konnte oder tatsächlich einen Pfad.
Damit wird der Aufrufer gezwungen, den Rückgabewert vor der Verwendung zu untersuchen.
Allein in dieser einen kurzen Methode macht null
so viele extra Prüfungen notwendig
und den Code dadurch schwerer lesbar und fehleranfälliger! null
wird als (unvollständige)
Initialisierung und als Rückgabewert und für den Fehlerfall genutzt, zusätzlich ist
die Semantik von null
nicht immer klar.
(Anmerkung: Der Gebrauch von null
hat nicht wirklich etwas mit "der Natur eines ECS"
zu tun. Die Methode wurde mittlerweile komplett überarbeitet und ist in der hier gezeigten
Form glücklicherweise nicht mehr zu finden.)
Entsprechend hat sich in diesem Review die nachfolgende Diskussion ergeben:
Erzeugen von Optional-Objekten
Konstruktor ist private
...
-
"Kein Wert":
Optional.empty()
-
Verpacken eines non-
null
Elements:Optional.of()
(NullPointerException
wenn Argumentnull
!) -
Verpacken eines "unsicheren"/beliebigen Elements:
Optional.ofNullable()
- Liefert verpacktes Element, oder
Optional.empty()
, falls Elementnull
war
Es sollte in der Praxis eigentlich nur wenige Fälle geben, wo ein Aufruf von
Optional.of()
sinnvoll ist. Ebenso ist Optional.empty()
nur selten sinnvoll.
Stattdessen sollte stets Optional.ofNullable()
verwendet werden.
null
kann nicht nicht in Optional<T>
verpackt werden!
(Das wäre dann eben Optional.empty()
.)
LSF liefert jetzt Optional zurück
public class LSF {
private Set<Studi> sl;
public Optional<Studi> getBestStudi() throws NullPointerException {
// Fehler: Es gibt noch keine Sammlung
if (sl == null) throw new NullPointerException("There ain't any collection");
Studi best = null;
for (Studi s : sl) {
if (best == null) best = s;
if (best.credits() < s.credits()) best = s;
}
// Entweder Optional.empty() (wenn best==null) oder Optional.of(best) sonst
return Optional.ofNullable(best);
}
}
Das Beispiel soll verdeutlichen, dass man im Fehlerfall nicht einfach null
oder
Optional.empty()
zurückliefern soll, sondern eine passende Exception werfen soll.
Wenn die Liste aber leer ist, stellt dies keinen Fehler dar! Es handelt sich um den
Fall "kein Wert vorhanden". In diesem Fall wird statt null
nun ein Optional.empty()
zurückgeliefert, also ein Objekt, auf dem der Aufrufer die üblichen Methoden aufrufen
kann.
Zugriff auf Optional-Objekte
In der funktionalen Programmierung gibt es schon lange das Konzept von Optional
,
in Haskell ist dies beispielsweise die Monade Maybe
. Allerdings ist die Einbettung
in die Sprache von vornherein mit berücksichtigt worden, insbesondere kann man hier
sehr gut mit Pattern Matching in der Funktionsdefinition auf den verpackten Inhalt
reagieren.
In Java gibt es die Methode Optional#isEmpty()
, die einen Boolean zurückliefert und
prüft, ob es sich um ein leeres Optional
handelt oder ob hier ein Wert "verpackt" ist.
Für den direkten Zugriff auf die Werte gibt es die Methoden Optional#orElseThrow()
und Optional#orElse()
. Damit kann man auf den verpackten Wert zugreifen, oder es
wird eine Exception geworfen bzw. ein Ersatzwert geliefert.
Zusätzlich gibt es Optional#isPresent()
, die als Parameter ein java.util.function.Consumer
erwartet, also ein funktionales Interface mit einer Methode void accept(T)
, die das
Objekt verarbeitet.
Studi best;
// Testen und dann verwenden
if (!lsf.getBestStudi().isEmpty()) {
best = lsf.getBestStudi().get();
// mach was mit dem Studi ...
}
// Arbeite mit Consumer
lsf.getBestStudi().ifPresent(studi -> {
// mach was mit dem Studi ...
});
// Studi oder Alternative (wenn Optional.empty())
best = lsf.getBestStudi().orElse(anne);
// Studi oder NoSuchElementException (wenn Optional.empty())
best = lsf.getBestStudi().orElseThrow();
Es gibt noch eine Methode get()
, die so verhält wie orElseThrow()
. Da man diese
Methode vom Namen her schnell mit einem Getter verwechselt, ist sie mittlerweile
deprecated.
Anmerkung: Da getBestStudi()
eine NullPointerException
werfen kann, sollte der
Aufruf möglicherweise in ein try/catch
verpackt werden. Dito für orElseThrow()
.
Einsatz mit Stream-API
public class LSF {
...
public Optional<Studi> getBestStudi() throws NullPointerException {
if (sl == null) throw new NullPointerException("There ain't any collection");
return sl.stream()
.sorted((s1, s2) -> s2.credits() - s1.credits())
.findFirst();
}
}
public static void main(String... args) {
...
String name = lsf.getBestStudi()
.map(Studi::name)
.orElseThrow();
}
Im Beispiel wird in getBestStudi()
die Sammlung als Stream betrachtet, über die
Methode sorted()
und den Lamda-Ausdruck für den Comparator
sortiert ("falsch"
herum: absteigend in den Credits der Studis in der Sammlung), und findFirst()
ist die terminale Operation auf dem Stream, die ein Optional<Studi>
zurückliefert:
entweder den Studi mit den meisten Credits (verpackt in Optional<Studi>
) oder
Optional.empty()
, wenn es überhaupt keine Studis in der Sammlung gab.
In main()
wird dieses Optional<Studi>
mit den Stream-Methoden von Optional<T>
bearbeitet, zunächst mit Optional#map()
. Man braucht nicht selbst prüfen, ob das
von getBestStudi()
erhaltene Objekt leer ist oder nicht, da dies von Optional#map()
erledigt wird: Es wendet die Methodenreferenz auf den verpackten Wert an (sofern
dieser vorhanden ist) und liefert damit den Namen des Studis als Optional<String>
verpackt zurück. Wenn es keinen Wert, also nur Optional.empty()
von getBestStudi()
gab, dann ist der Rückgabewert von Optional#map()
ein Optional.empty()
. Wenn
der Name, also der Rückgabewert von Studi::name
, null
war, dann wird ebenfalls
ein Optional.empty()
zurückgeliefert. Dadurch wirft orElseThrow()
dann eine
NoSuchElementException
. Man kann also direkt mit dem String name
weiterarbeiten
ohne extra null
-Prüfung - allerdings will man noch ein Exception-Handling einbauen
(dies fehlt im obigen Beispiel aus Gründen der Übersicht) ...
Weitere Optionals
Für die drei primitiven Datentypen int
, long
und double
gibt es passende
Wrapper-Klassen von Optional<T>
: OptionalInt
, OptionalLong
und OptionalDouble
.
Diese verhalten sich analog zu Optional<T>
, haben aber keine Methode ofNullable()
,
da dies hier keinen Sinn ergeben würde: Die drei primitiven Datentypen repräsentieren
Werte - diese können nicht null
sein.
Regeln für Optional
-
Nutze
Optional
nur als Rückgabe für "kein Wert vorhanden"Optional
ist nicht als Ersatz für einenull
-Prüfung o.ä. gedacht, sondern als Repräsentation, um auch ein "kein Wert vorhanden" zurückliefern zu können. -
Nutze nie
null
für eineOptional
-Variable oder einenOptional
-RückgabewertWenn man ein
Optional
als Rückgabe bekommt, sollte das niemals selbst einenull
-Referenz sein. Das macht das gesamte Konzept kaputt!Nutzen Sie stattdessen
Optional.empty()
. -
Nutze
Optional.ofNullable()
zum Erzeugen einesOptional
Diese Methode verhält sich "freundlich" und erzeugt automatisch ein
Optional.empty()
, wenn das Argumentnull
ist. Es gibt also keinen Grund, dies mit einer Fallunterscheidung selbst erledigen zu wollen.Bevorzugen Sie
Optional.ofNullable()
vor einer manuellen Fallunterscheidung und dem entsprechenden Einsatz vonOptional.of()
undOptional.empty()
. -
Erzeuge keine
Optional
als Ersatz für die Prüfung aufnull
Wenn Sie auf
null
prüfen müssen, müssen Sie aufnull
prüfen. Der ersatzweise Einsatz vonOptional
macht es nur komplexer - prüfen müssen Sie hinterher ja immer noch. -
Nutze
Optional
nicht in Attributen, Methoden-Parametern und SammlungenNutzen Sie
Optional
vor allem für Rückgabewerte.Attribute sollten immer direkt einen Wert haben oder
null
, analog Parameter von Methoden o.ä. ... Hier hilftOptional
nicht, Sie müssten ja trotzdem einenull
-Prüfung machen, nur eben dann über denOptional
, wodurch dies komplexer und schlechter lesbar wird.Aus einem ähnlichen Grund sollten Sie auch in Sammlungen keine
Optional
speichern! -
Vermeide den direkten Zugriff (
ifPresent()
,orElseThrow()
...)Der direkte Zugriff auf ein
Optional
entspricht dem Prüfen aufnull
und dann dem Auspacken. Dies ist nicht nur Overhead, sondern auch schlechter lesbar.Vermeiden Sie den direkten Zugriff und nutzen Sie
Optional
mit den Stream-Methoden. So ist dies von den Designern gedacht.
Interessante Links
- "Using Optionals"
- "What You Might Not Know About Optional"
- "Experienced Developers Use These 7 Java Optional Tips to Remove Code Clutter"
- "Code Smells: Null"
- "Class Optional"
Wrap-Up
Optional
als Rückgabe für "kein Wert vorhanden"
-
Optional.ofNullable()
: Erzeugen einesOptional
- Entweder Objekt "verpackt" (Argument !=
null
) - Oder
Optional.empty()
(Argument ==null
)
- Entweder Objekt "verpackt" (Argument !=
-
Prüfen mit
isEmpty()
undifPresent()
-
Direkter Zugriff mit
ifPresent()
,orElse()
undorElseThrow()
-
Stream-API:
map()
,filter()
,flatMap()
, ... -
Attribute, Parameter und Sammlungen: nicht
Optional
nutzen -
Kein Ersatz für
null
-Prüfung!
Schöne Doku: "Using Optionals".
String-Handling
Können Sie den folgenden Code so umschreiben, dass Sie statt der if
-Abfragen und der einzelnen direkten Methodenaufrufe
die Stream-API und Optional<T>
nutzen?
String format(final String text, String replacement) {
if (text.isEmpty()) {
return "";
}
final String trimmed = text.trim();
final String withSpacesReplaced = trimmed.replaceAll(" +", replacement);
return replacement + withSpacesReplaced + replacement;
}
Ein Aufruf format(" Hello World ... ", "_");
liefert den String "_Hello_World_..._
".
- [LernJava] Learn Java
Oracle Corporation, 2022.
Tutorials \> The Stream API \> Using Optionals - [Ullenboom2021] Java ist auch eine Insel
Ullenboom, C., Rheinwerk-Verlag, 2021. ISBN 978-3-8362-8745-6.
Kap. 12.6: Optional ist keine Nullnummer