Optional Einführung: Unterschied zwischen den Versionen

Aus MimiPedia
 
(7 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt)
Zeile 16: Zeile 16:
Man darf sich Optionals allerdings nicht als Daten-Container vorstellen, der der dauerhaften Speicherung dient.
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.
Optionals sind Helfer bei der Verarbeitung und Manipulation von Objekten.
Eine etwas ausführlichere Begründung findet sich [hier|| nicht serialisierbar].
Eine Anmerkung zur Serialisierbarkeit findet sich [https://mletkin.org/warum-optionals-nicht-serialisierbar-sind/ hier].


der Einfachheit halber betrachten wir im Folgenden ausschließlich Optionals die Strings einwickeln.
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,
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.
daß {{lambda}}-Ausdrücke ''keine'' Klassen-Instanzen sind und nicht ver-optionalt werden können.


== Optionals erzeugen ==
== Optionals erzeugen ==
Um einen Wet in ein {{java|Optional}}-Objekt zu verpacken muß man eines erzeugen.
Um einen Wert in ein {{java|Optional}}-Objekt zu verpacken muß man eines erzeugen.
Man kann das mit Hilfe dreier statischer Methoden erreichen:
Man kann das mit Hilfe dreier statischer Methoden erreichen:


Zeile 64: Zeile 64:
mit der Methode {{java|isPresent()}}. Sodann möchten wir auf den Inhalt zugreifen, wozu die Methode {{java|get()}} dient.
mit der Methode {{java|isPresent()}}. Sodann möchten wir auf den Inhalt zugreifen, wozu die Methode {{java|get()}} dient.
Das geht allerdings nur, wenn der Wert ungleich {{java|null}} ist, andernfalls giebt's eine Exception. Im folgenden Beispiel
Das geht allerdings nur, wenn der Wert ungleich {{java|null}} ist, andernfalls giebt's eine Exception. Im folgenden Beispiel
prüfen wir und liefern dann den Wert oder -- im {{java|null}}-Falle -- den default.Wert "nichts".
prüfen wir und liefern dann den Wert oder -- im {{java|null}}-Falle -- den default-Wert "nichts".
{{java|code=
{{java|code=
Optional<String> zahl = Optional.of("sechs");
Optional<String> zahl = Optional.of("sechs");
Zeile 86: Zeile 86:
dann fällt sogar das qualifizierende "Optional" weg.
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.
Für die folgenden Features wird ein grundsätzliches Verständnis von {{lambda}}-Ausdrücken benötigt.


Eine kurze Einführung giebt's [[Lambdas in Java|hier]].
Eine kurze Einführung giebt's [[Java-Lambdas Einführung|hier]].


== Nur ausführen, wenn nicht null ==
== Nur ausführen, wenn nicht null ==
Zeile 137: Zeile 137:
     .orElse(Flasche.EMPTY);
     .orElse(Flasche.EMPTY);
}}
}}
Die Methode {{java|map()}} wendet den – als Parameter übergebenen – &lambda;-Ausdruck auf den Wert an, der im Optional verpackt ist. Das aber nur, wenn er nicht {{java|null}} ist. Das Ergebnis wird in ein Optional verpackt und weitergegeben. Das bedeutet, daß sich durch jede Anwendung von {{java|map()}} der Typ des Optionals ändert. Zerlegen wir mal das Beispiel:
Die Methode {{java|map()}} wendet den – als Parameter übergebenen – {{lambda}}-Ausdruck auf den Wert an, der im Optional verpackt ist. Das aber nur, wenn er nicht {{java|null}} ist. Das Ergebnis wird in ein Optional verpackt und weitergegeben. Das bedeutet, daß sich durch jede Anwendung von {{java|map()}} der Typ des Optionals ändert. Zerlegen wir mal das Beispiel:
{| class="wikitable"
{| class="wikitable"
!
!
Zeile 172: Zeile 172:
Speziell für das Werfen von Exceptions gibt es eine eigene Methode:
Speziell für das Werfen von Exceptions gibt es eine eigene Methode:
{{java|code=return eingabe.orElseThrow((x) -> new IllegalArgumentException("giebt's nich")));}}
{{java|code=return eingabe.orElseThrow((x) -> new IllegalArgumentException("giebt's nich")));}}
Wenn der Konstruktor der Exception keine Parameter hat, kann man statt dessen auch {{java|orElseGet()}} verwenden:
Wenn der Konstruktor der Exception keine Parameter hat, kann man für den Aufruf auch die {{java|::}}-Form verwenden.
{{java|code=return eingabe.orElse(IllegalArgumentException::new);}}
{{java|code=return eingabe.orElseThrow(IllegalArgumentException::new);}}

Aktuelle Version vom 24. Mai 2022, 18:58 Uhr

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 Anmerkung zur Serialisierbarkeit findet sich hier.

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 λ-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);