Java Optional
Drücken Sie das mögliche Fehlen eines Werts in Java mit Optional aus und vermeiden Sie NullPointerException by Design.
Optional<T> ist ein Container, der entweder einen Wert vom Typ T oder nichts enthält — und Ihnen auf Typebene mitteilt, welches der Fall ist, sodass der Compiler Sie zwingen kann, den fehlenden Fall zu behandeln. Es wurde in Java 8 zusammen mit Streams hinzugefügt, und beide sind so konzipiert, dass sie zusammenpassen: findFirst, findAny, min, max, reduce geben alle Optional<T> zurück, genau weil die Antwort möglicherweise nicht existiert, und die API bietet Ihnen fließende Möglichkeiten, weiterzurechnen, ohne jemals if (x != null) zu schreiben.
Optional ist kein universeller Ersatz für null, und das JDK hat klare Vorstellungen davon, wo es hingehört. Dieses Kapitel führt durch die gesamte API und zeigt dann die drei Stellen, an denen Optional die falsche Wahl ist.
Erstellen eines Optional
Drei Konstruktoren, jeder mit einer präzisen Bedeutung:
Optional<String> a = Optional.of("hello"); // present; null arg throws NPE
Optional<String> b = Optional.empty(); // absent
Optional<String> c = Optional.ofNullable(maybeNull); // present if non-null, else emptyDer Unterschied ist wichtig. Optional.of(x) ist die Aussage „Dieser Wert ist definitiv vorhanden" — wenn Sie null übergeben, wird sofort NullPointerException geworfen, was das Gewünschte ist (ein Fehler an der Quelle, nicht drei Frames weiter unten). Optional.ofNullable(x) ist der Adapter, den Sie um eine Legacy-API wickeln, die null für „nicht vorhanden" zurückgibt.
In einer Stream-Pipeline erstellen Sie ein Optional fast nie von Hand — Terminale wie findFirst und Collectors.maxBy erzeugen sie für Sie.
Abfragen, ob ein Wert vorhanden ist
Die zwei Abfragen:
Optional<String> opt = lookup(id);
boolean has = opt.isPresent(); // true if a value is held
boolean none = opt.isEmpty(); // Java 11+ -- the opposite of isPresentDiese erscheinen in Produktionscode, sind aber meist ein Code Smell: Der meiste Code, der isPresent und dann get aufruft, wäre mit einer der unten beschriebenen operate-on-it-Methoden lesbarer. Die Abfragemethoden sind für Grenzcode gedacht, bei dem Sie wirklich einen boolean benötigen — eine Guard-Klausel, eine Routing-Entscheidung, ein Logging-Zweig.
Den Wert sicher lesen
Der falsche Weg:
String name = opt.get(); // throws NoSuchElementException if emptyopt.get() ist das unkontrollierte Lesen. Damit wandeln Sie ein Optional zurück in einen Wert und eine Laufzeitausnahme um — genau das, was der Typ verhindern sollte. Verwenden Sie es nur, nachdem Sie bewiesen haben, dass das Optional vorhanden ist (oder nach findFirst().orElseThrow() aus einer Pipeline, bei der ein leeres Ergebnis ein Programmiererfehler wäre, kein erwarteter Fall).
Die richtigen Wege, in Reihenfolge der Präferenz:
String name1 = opt.orElse("anonymous"); // default value
String name2 = opt.orElseGet(() -> expensiveDefault()); // lazy default
String name3 = opt.orElseThrow(); // NoSuchElementException
String name4 = opt.orElseThrow(() -> new MyDomainError(id)); // custom exceptionorElse(value)— liefert einen Standardwert. Der Wert wird immer ausgewertet, auch wenn das Optional vorhanden ist. Übergeben Sie daher keinen teuren Ausdruck.orElseGet(supplier)— liefert einen Standardwert lazy. Der Supplier läuft nur, wenn das Optional leer ist. Verwenden Sie dies für jeden Standard, der mehr kostet als ein Literal.orElseThrow()— wirftNoSuchElementExceptionwenn abwesend. Die parameterlose Form ab Java 10 ist das moderne Äquivalent vonopt.get(), wenn „dies muss definitiv vorhanden sein" die einzig sinnvolle Interpretation an der Aufrufstelle ist.orElseThrow(supplier)— wirft eine domänenspezifische Ausnahme. Der Standardweg, um „abwesend" in „404 not found" zu übersetzen.
Den Wert transformieren — map
Wenn das Optional vorhanden ist, eine Funktion anwenden; andernfalls leer bleiben:
Optional<String> upper = opt.map(String::toUpperCase);
Optional<Integer> len = opt.map(String::length);Die Signatur lautet Optional<T>.map(Function<T, R>) -> Optional<R>. Die Funktion wird nur ausgeführt, wenn ein Wert vorhanden ist — kein null-Check, kein if, kein else. Dies ist die Operation, die Optional seinen Zeichenaufwand wert macht: Die meisten Ketten von „wenn nicht-null, tue dies; wenn nicht-null, tue dann dies" kollabieren zu .map(...).map(...).map(...).
Es gibt einen Sonderfall, den das JDK stillschweigend behandelt: Wenn Ihre map-Funktion null zurückgibt (weil sie eine Legacy-API umhüllt, die null für „kein Ergebnis" zurückgibt), ist das resultierende Optional empty() — nicht Optional.of(null).
Optionals zusammensetzen — flatMap
Wenn die Mapping-Funktion selbst ein Optional zurückgibt, würde map ein Optional<Optional<T>> erzeugen. flatMap flacht es ab:
record User(String id, Optional<Address> address) {}
record Address(String city) {}
Optional<String> city = userById(id)
.flatMap(User::address) // Optional<Address>
.map(Address::city); // Optional<String>flatMap ist die Operation, die es Ihnen ermöglicht, mehrere Lookups — von denen jeder fehlschlagen kann — in einer einzigen Pipeline zu verketten. Beide Fehlerfälle kollabieren am Ende zu Optional.empty(), und der Consumer behandelt sie einmal mit orElse / orElseThrow.
Filtern — filter
Prüft den Wert gegen ein Predicate<T>; gibt dasselbe Optional zurück, wenn es besteht, empty() wenn nicht:
Optional<String> nonBlank = opt.filter(s -> !s.isBlank());
Optional<Integer> positive = numberOpt.filter(n -> n > 0);Wirkt als Guard innerhalb der Optional-Pipeline. Nützlich, wenn die Frage lautet: „Ich habe einen Wert, aber ist er der richtige Wert, um fortzufahren?"
Seiteneffekte — ifPresent, ifPresentOrElse
Code nur ausführen, wenn der Wert vorhanden ist:
opt.ifPresent(name -> log.info("hello, {}", name));Oder einen Zweig ausführen, wenn vorhanden, und einen anderen, wenn leer (Java 9+):
opt.ifPresentOrElse(
name -> log.info("hello, {}", name),
() -> log.warn("no name on the request"));Dies ist der richtige Weg, um „tue etwas nebenbei" auszudrücken. Sie ersetzen das Muster if (opt.isPresent()) { use(opt.get()); } vollständig.
Brücke zu Streams — Optional.stream()
(Java 9+) Wandelt ein Optional<T> in einen Stream<T> mit null oder einem Element um:
Stream<String> s = opt.stream();Nützlich innerhalb von flatMap auf einem Stream<Optional<T>>:
List<String> presentCities = userIds.stream()
.map(this::userById) // Stream<Optional<User>>
.flatMap(Optional::stream) // Stream<User> -- empties drop, presents pass through
.map(User::city)
.toList();Das ersetzt filter(Optional::isPresent).map(Optional::get) durch ein einzelnes flatMap(Optional::stream). Gleiches Ergebnis, sauberere Pipeline.
or — Rückfall auf ein anderes Optional
(Java 9+) Wenn leer, einen Supplier eines anderen Optional verwenden:
Optional<User> u = primaryLookup(id)
.or(() -> fallbackLookup(id))
.or(() -> Optional.of(User.anonymous()));Liest sich als „versuche primär; wenn abwesend, versuche Fallback; wenn abwesend, verwende anonym." Alle drei sind Optional<User>; die Kette gibt das erste nicht-leere zurück. Unterschied zu orElse — or hält das Ergebnis eingewickelt; orElse wickelt es mit einem einfachen T-Standard aus.
Primitive Spezialisierungen
Es gibt OptionalInt, OptionalLong, OptionalDouble für primitive Ergebnisse — was z. B. IntStream.max() zurückgibt:
OptionalInt max = nums.stream().mapToInt(Integer::intValue).max();
int hi = max.orElse(0);Sie haben eine kleinere API — kein map/flatMap/filter — weil sie an der Grenze der primitiven Welt sitzen. Verwenden Sie sie, um primitive-Stream-Ergebnisse zu lesen; konvertieren Sie zu Optional<Integer>, wenn Sie die vollständige API benötigen.
Wo Optional nicht hingehört
Die Designabsicht des JDK ist eng gefasst: Optional ist ein Rückgabetyp für Methoden, deren Antwort möglicherweise nicht existiert. Es ist kein:
- Feldtyp. Schreiben Sie nicht
private Optional<String> middleName;. Es ist nichtSerializable, kostet pro Feld eine Allokation, und einnull-Feld ist kürzer und klarer für „Diese Entität hat keinen zweiten Vornamen." Die richtige Vorgehensweise ist ein nicht-Optional-Feld, dasnullsein kann, mit einem Getter, derOptionalzurückgibt. - Methodenparameter. Akzeptieren Sie kein
Optional<String>als Argument. Überladen Sie die Methode, oder akzeptieren SieStringund dokumentieren Sie, dassnull„abwesend" bedeutet. Optionale Parameter zwingen den Aufrufer zum Einwickeln, was Rauschen erzeugt. - Collection-Element.
List<Optional<T>>ist fast immer eine Liste mit nullable Elementen und zusätzlichem Einwickeln. Verwenden SieList<T>und filtern Sie die Nulls an der Grenze heraus, oder verwenden SieflatMap(Optional::stream), um die Abwesenden in einer Pipeline zu verwerfen. - Ein Weg, alle
nullzu vermeiden. Java hat immer nochnullin jedem Referenztyp;Optionalist für die Rückgabeform von Code, der Werte produziert, die möglicherweise nicht existieren. Einfache Referenztypen sind für alles andere in Ordnung.
Die kürzere Regel: Ein Optional, das aus einer Methode fließt, ist gutes Design; ein Optional, das hinein fließt, ist fast immer falsch.
Ein ausgearbeitetes Beispiel: jede Methode und die Faustregeln im Code
Das folgende Programm erstellt einen kleinen Benutzer/Adress-Graphen, durchläuft jede Methode von Optional dagegen, demonstriert das Auswertungstiming von orElse vs. orElseGet, die Optional.stream()-Brücke und die or-Kette.
Was aus dem Durchlauf zu entnehmen ist:
- Die drei Konstruktoren
of,empty,ofNullablebilden drei klare Absichten ab: definitiv vorhanden, definitiv abwesend und Legacy-Adapter, vorhanden-wenn-nicht-null.Optional.of(null)wirft — und das ist der gewünschte Fehler, kein Bug, den man umgehen sollte. orElsewertete sein Argument jedes Mal aus, auch wenn das Optional vorhanden war. Der Supplier vonorElseGetlief nur bei Bedarf. Verwenden SieorElsefür günstige Literale undorElseGetfür alles, was alloziert, abfragt oder wirft.mapundflatMapließen die gesamte KetteuserById(...).flatMap(User::address).map(Address::city)als einzelne Pipeline lesen — keinenull-Checks, keine verschachtelten Ifs, und jeder leere Schritt kurzschließt am Ende zuOptional.empty().flatMap(Optional::stream)verwandelte einenStream<Optional<User>>in einenStream<User>, wobei alle Abwesenden in einem Schritt verworfen wurden. Das ist der saubere Weg, eine Liste von „kann-scheitern"-Lookups in einen Stream von Erfolgen zu überbrücken.OptionalIntist das, was primitive-Stream-Terminale wieIntStream.findFirstzurückgeben. Es hat seine eigene kleine API (getAsInt,orElse,ifPresent) und existiert, damit primitive Pipelines nie boxen müssen.- Die Faustregel für „falsche Orte" tauchte implizit auf:
User.addresswar einOptional<Address>-Feld — für das Beispiel in Ordnung, da es die API demonstrieren wollte, aber im Produktionscode wäre das Feld ein möglicherweise-null-Addressmit einemOptional<Address> address()-Getter, der das Einwickeln übernimmt.
Wie es weitergeht
Teil 12 behandelte das funktionale Vokabular von Anfang bis Ende: funktionale Interfaces, Lambdas, Methodenreferenzen, die eingebauten Typen, die Stream-Pipeline, jede Quelle, jedes Intermediate, jedes Terminal, Collectors, parallele Ausführung und schließlich Optional als Ausdruck von Abwesenheit auf Typebene. Das nächste Kapitel, Java Predicate Interface, zoomt auf ein einzelnes funktionales Interface zurück — Predicate<T> — und die Kombinator-Algebra (and, or, negate, isEqual, not), die es Ihnen ermöglicht, Prädikate zusammenzusetzen, ohne jemals den booleschen Klebstoff von Hand zu schreiben. Von dort aus setzt sich der Teil mit Function, Consumer/Supplier und der binären Operator-Familie fort — ein Interface pro Kapitel, jedes mit der gleichen Beispielstruktur, die Sie hier gesehen haben.