Zum Inhalt springen

Java Optional (mit Beispielen)

Drei durchsichtige Glaswürfel, pink und blau leuchtend im Dunkeln

Ein Optional ist ein Container, der entweder genau einen Wert enthält oder keinen. Eine Methode liefert ein Optional zurück, wenn es sein kann, dass es kein Ergebnis gibt – statt null zurückzugeben.

Ein Beispiel: Stream.findFirst() liefert ein Optional<Book>, denn der Stream kann leer sein, oder keines seiner Bücher kommt durch den Filter:

Optional<Book> firstGothic = BOOKS.stream()
    .filter(book -> book.genre() == GOTHIC)
    .findFirst();

Code, der findFirst() aufruft, kann das fehlende Ergebnis nicht übersehen: Um an das Buch zu kommen, muss er festlegen, was passieren soll, wenn es keines gibt.

Optional gibt es in Java seit Java 8 (März 2014) – vorher zeigte eine Methode ein fehlendes Ergebnis mit null an, und ob sie null zurückgeben konnte, stand bestenfalls in ihrem Javadoc. Übersah der aufrufende Code das null, führte das zu einer NullPointerException. Keine Sorge – das zeige ich dir gleich alles an Beispielen.

In diesem Artikel erfährst du,

  • was ein Optional ist und welches Problem es löst,
  • wie du ein Optional mit of(), ofNullable() und empty() erzeugst,
  • wie du den Wert mit orElse(), orElseGet() und orElseThrow() herausholst – und worin sich orElse() und orElseGet() unterscheiden,
  • wie du ein Optional mit map(), flatMap(), filter() und or() umformst,
  • wie Optional und Streams zusammenarbeiten,
  • wann du Optional einsetzen solltest – und wann nicht,
  • was „Value-based Class“ bedeutet und was Project Valhalla an Optional ändert,
  • welche Fehler du vermeiden solltest.

Die Beispiele in diesem Artikel

Die Beispiele verwenden das Datenmodell des Artikels über Java Streams – ein Enum Genre, einen Record Book und eine kleine Bibliothek mit elf Klassikern:

public enum Genre {
  NOVEL,
  GOTHIC,
  ADVENTURE,
  FANTASY,
  SCIENCE_FICTION
}

public record Book(String title, String author, int year, Genre genre) {}
public class Library {

  public static final List<Book> BOOKS = List.of(
      new Book("Pride and Prejudice", "Jane Austen", 1813, NOVEL),
      new Book("Frankenstein", "Mary Shelley", 1818, GOTHIC),
      new Book("Moby-Dick", "Herman Melville", 1851, ADVENTURE),
      new Book("From the Earth to the Moon", "Jules Verne", 1865, SCIENCE_FICTION),
      new Book("Alice's Adventures in Wonderland", "Lewis Carroll", 1865, FANTASY),
      new Book("Around the World in Eighty Days", "Jules Verne", 1873, ADVENTURE),
      new Book("Treasure Island", "Robert Louis Stevenson", 1883, ADVENTURE),
      new Book("Kidnapped", "Robert Louis Stevenson", 1886, ADVENTURE),
      new Book("The Time Machine", "H. G. Wells", 1895, SCIENCE_FICTION),
      new Book("Dracula", "Bram Stoker", 1897, GOTHIC),
      new Book("The War of the Worlds", "H. G. Wells", 1898, SCIENCE_FICTION));
}

Für diesen Artikel bekommt Library zwei Methoden, die ein Optional liefern.

findByTitle() liefert das Buch mit genau dem angegebenen Titel:

public static Optional<Book> findByTitle(String title) {
  return BOOKS.stream()
      .filter(book -> book.title().equals(title))
      .findFirst();
}

nextBookBy() liefert das Buch, das derselbe Autor als Nächstes veröffentlicht hat:

public static Optional<Book> nextBookBy(Book book) {
  return BOOKS.stream()
      .filter(otherBook -> otherBook.author().equals(book.author()))
      .filter(otherBook -> otherBook.year() > book.year())
      .min(Comparator.comparingInt(Book::year));
}

Für „Treasure Island“ (1883) ist das „Kidnapped“ (1886). Für „Dracula“ hat die Bibliothek kein späteres Buch von Bram Stoker, das Optional ist also leer.

In den folgenden Codebeispielen rufe ich beide Methoden ohne Klassennamen auf – sie sind statisch importiert. Den vollständigen Code aller Beispiele findest du im GitHub-Repository java-streams-examples, im Package eu.happycoders.optional.

Was ist ein Optional in Java?

Angenommen, die Suche nach einem Titel wäre so geschrieben, wie man Methoden vor Java 8 geschrieben hat – getByTitle() liefert das Buch oder, wenn es keines gibt, null:

static Book getByTitle(String title) {
  for (Book book : BOOKS) {
    if (book.title().equals(title)) {
      return book;
    }
  }
  return null;
}

Die Signatur Book getByTitle(String title) verrät nicht, dass die Methode null zurückgeben kann.

Der Compiler weiß es auch nicht, und so kompiliert dieser Code:

Book book = getByTitle("Ulysses");
System.out.println(book.year());

Du merkst es erst zur Laufzeit:

java.lang.NullPointerException: Cannot invoke "eu.happycoders.streams.Book.year()" because "book" is null

Mit Optional<Book> als Rückgabetyp sagt es die Signatur: Es kann sein, dass es kein Buch gibt. Und auf einem Optional<Book> kannst du year() gar nicht aufrufen – Optional hat keine solche Methode. Um an das Buch zu kommen, musst du das Optional auspacken und festlegen, was passiert, wenn es leer ist.

Du kannst dir ein Optional als Schachtel vorstellen. Für „Dracula“ liefert findByTitle() eine Schachtel mit dem Buch darin, für „Ulysses“ eine leere Schachtel. Die Schachtel selbst ist immer da:

Zwei Aufrufe von findByTitle() und ihre Ergebnisse: findByTitle("Dracula") liefert ein Optional<Book>, das das Buch Dracula von Bram Stoker, 1897, enthält; findByTitle("Ulysses") liefert ein Optional<Book>, das keinen Wert enthält
Ein Optional enthält entweder genau einen Wert oder keinen – das Optional selbst ist nie null

toString() gibt die beiden Optionals so aus:

System.out.println(findByTitle("Dracula"));
System.out.println(findByTitle("Ulysses"));
Optional[Book[title=Dracula, author=Bram Stoker, year=1897, genre=GOTHIC]]
Optional.empty

Ein Optional enthält nie null: Ein Optional, das aus null erzeugt wird, ist leer („empty“).

Ein Optional erzeugen

Optional.of() und Optional.empty()

Optional.of() verpackt einen Wert, der nicht null ist; Optional.empty() liefert ein leeres Optional:

Optional<String> title = Optional.of("Dracula");
Optional<String> noTitle = Optional.empty();

System.out.println(title);
System.out.println(noTitle);
Optional[Dracula]
Optional.empty

Optional.ofNullable()

Optional.ofNullable() nimmt auch null an und liefert dann ein leeres Optional.

Du brauchst es dort, wo ein Wert aus einer API kommt, die „kein Ergebnis“ mit null anzeigt – z. B. Map.get():

Map<String, Book> byTitle = new HashMap<>();
for (Book book : BOOKS) {
  byTitle.put(book.title(), book);
}

System.out.println(Optional.ofNullable(byTitle.get("Dracula")));
System.out.println(Optional.ofNullable(byTitle.get("Ulysses")));
Optional[Book[title=Dracula, author=Bram Stoker, year=1897, genre=GOTHIC]]
Optional.empty

Optional.of() hingegen wirft sofort eine NullPointerException, wenn du null übergibst:

Optional<Book> book = Optional.of(byTitle.get("Ulysses"));
java.lang.NullPointerException

Warum dann nicht immer ofNullable()? Weil of() etwas aussagt: Dieser Wert ist nie null. Ist er es doch, ist das ein Bug, und die NullPointerException zeigt auf die Zeile, in der er passiert – nicht auf eine Stelle drei Methoden später, an der unerwartet ein leeres Optional auftaucht.

Ich empfehle of() überall dort, wo der Wert nicht null sein darf, und ofNullable() nur dort, wo null eine legitime Antwort ist.

Den Wert aus einem Optional herausholen

orElse() und orElseGet()

orElse() liefert den Wert, wenn es einen gibt, und sonst den Fallback-Wert, den du übergibst.

Im folgenden Listing macht map(Book::title) zuerst aus dem Optional<Book> ein Optional<String> mit dem Titel – mehr dazu im Abschnitt map():

String title = findByTitle("Dracula")
    .map(Book::title)
    .orElse("(unknown)");
System.out.println(title);

String missing = findByTitle("Ulysses")
    .map(Book::title)
    .orElse("(unknown)");
System.out.println(missing);
Dracula
(unknown)

orElseGet() macht dasselbe, nimmt aber einen Supplier entgegen, der den Fallback-Wert erzeugt. Wo ist der Unterschied? orElse() bekommt den Fallback-Wert als Argument – und Java wertet Argumente vor dem Methodenaufruf aus, egal ob das Optional einen Wert enthält oder nicht. Der Supplier, den orElseGet() entgegennimmt, wird dagegen nur dann aufgerufen, wenn das Optional leer ist.

Die folgende Methode macht das sichtbar, denn sie gibt bei jedem Aufruf eine Zeile aus:

static String fallbackTitle() {
  System.out.println("  computing the fallback title");
  return "(unknown)";
}

Mit orElse() läuft fallbackTitle(), obwohl „Dracula“ in der Bibliothek steht:

System.out.println(findByTitle("Dracula")
    .map(Book::title)
    .orElse(fallbackTitle()));
  computing the fallback title
Dracula

Mit orElseGet() läuft sie nur, wenn das Optional leer ist:

System.out.println(findByTitle("Dracula")
    .map(Book::title)
    .orElseGet(() -> fallbackTitle()));

System.out.println(findByTitle("Ulysses")
    .map(Book::title)
    .orElseGet(() -> fallbackTitle()));
Dracula
  computing the fallback title
(unknown)

Für eine Konstante oder einen Wert, den du schon hast, ist orElse() die richtige Wahl. Sobald der Fallback-Wert erst berechnet werden muss – z. B. durch eine Datenbankabfrage, durch die Konstruktion eines neuen Objekts oder die Zusammensetzung eines Strings –, empfehle ich orElseGet(), denn sonst erfolgt die Berechnung auch dann, wenn das Optional einen Wert enthält und der Fallback-Wert gar nicht benötigt wird.

orElseThrow()

Wenn ein fehlender Wert bedeutet, dass etwas schiefgelaufen ist, wirft orElseThrow() eine Exception. Ohne Argument – das gibt es seit Java 10 – wirft es eine NoSuchElementException:

Book book = findByTitle("Ulysses").orElseThrow();
java.util.NoSuchElementException: No value present

Mit einem Supplier bestimmst du die Exception selbst:

Book book = findByTitle("Ulysses")
    .orElseThrow(() -> new IllegalArgumentException("Unknown title: Ulysses"));
java.lang.IllegalArgumentException: Unknown title: Ulysses

Ich empfehle die Variante mit Supplier überall dort, wo die Exception bei jemandem ankommt, der sie verstehen muss: „Unknown title: Ulysses“ sagt, was schiefgelaufen ist, „No value present“ nicht.

isPresent(), isEmpty() und get()

isPresent() liefert true, wenn das Optional einen Wert enthält. isEmpty(), das es seit Java 11 gibt, liefert das Gegenteil.

get() liefert den Wert und wirft eine NoSuchElementException, wenn es keinen gibt. Rufe get() deshalb nur auf, wenn du vorher mit isPresent() geprüft hast, dass ein Wert vorhanden ist. Statische Codeanalyse-Tools wie SonarQube melden einen Aufruf von get() ohne diese Prüfung als Bug (Regel S3655, „Optional values should not be accessed when they may be empty“).

Das Javadoc von get() nennt orElseThrow() „the preferred alternative“, also die bevorzugte Alternative. orElseThrow() verhält sich genau wie get(), aber sein Name sagt, was passiert, wenn das Optional leer ist.

Das folgende Listing verwendet isPresent(), get() und isEmpty():

Optional<Book> dracula = findByTitle("Dracula");
if (dracula.isPresent()) {
  System.out.println(dracula.get().year());
}

System.out.println(findByTitle("Ulysses").isEmpty());
1897
true

Das funktioniert, ist aber die null-Prüfung in anderer Form. Warum ich das Paar isPresent() und get() meide, zeigt der Abschnitt isPresent() und get() statt map() und orElse().

Mit dem Wert arbeiten: ifPresent() und ifPresentOrElse()

ifPresent() führt eine Aktion mit dem Wert aus, aber nur, wenn es einen gibt:

findByTitle("Dracula")
    .ifPresent(book -> System.out.println("Found: " + book.title()));

findByTitle("Ulysses")
    .ifPresent(book -> System.out.println("Found: " + book.title()));
Found: Dracula

Der zweite Aufruf gibt nichts aus, denn „Ulysses“ steht nicht in der Bibliothek.

Seit Java 9 nimmt ifPresentOrElse() eine zweite Aktion entgegen, ein Runnable, für den Fall, dass das Optional leer ist:

findByTitle("Ulysses")
    .ifPresentOrElse(
        book -> System.out.println("Found: " + book.title()),
        () -> System.out.println("Not in the library"));
Not in the library

Ein Optional umformen

Die vier Methoden dieses Kapitels liefern jeweils wieder ein Optional. Du kannst sie deshalb verketten, wie die Operationen eines Streams. map(), flatMap() und filter() arbeiten dabei nur, wenn es einen Wert gibt – ein leeres Optional reichen sie unverändert weiter. or() greift umgekehrt nur, wenn das Optional leer ist.

map()

map() wendet eine Funktion auf den Wert an und verpackt das Ergebnis in ein neues Optional:

Optional<Integer> draculaYear = findByTitle("Dracula")
    .map(Book::year);
System.out.println(draculaYear);

Optional<Integer> ulyssesYear = findByTitle("Ulysses")
    .map(Book::year);
System.out.println(ulyssesYear);
Optional[1897]
Optional.empty

Bei einem leeren Optional ruft map() die Funktion gar nicht erst auf. Und liefert die Funktion null, gibt map() ein leeres Optional zurück – es verpackt das Ergebnis mit ofNullable().

flatMap()

Was passiert, wenn die Funktion selbst ein Optional liefert, wie nextBookBy()?

Dann verpackt map() dieses Optional in ein weiteres:

Optional<Optional<Book>> nestedAfterTreasureIsland = findByTitle("Treasure Island")
    .map(Library::nextBookBy);
System.out.println(nestedAfterTreasureIsland);

Optional<Optional<Book>> nestedAfterDracula = findByTitle("Dracula")
    .map(Library::nextBookBy);
System.out.println(nestedAfterDracula);
Optional[Optional[Book[title=Kidnapped, author=Robert Louis Stevenson, year=1886, genre=ADVENTURE]]]
Optional[Optional.empty]

Für „Dracula“ ist das Ergebnis nicht einmal leer, sondern ein Optional, das ein leeres Optional enthält.

Für diesen Fall gibt es flatMap(): Die Methode gibt das Optional der Funktion so zurück, wie es ist, ohne es noch einmal zu verpacken:

Optional<Book> afterTreasureIsland = findByTitle("Treasure Island")
    .flatMap(Library::nextBookBy);
System.out.println(afterTreasureIsland);

Optional<Book> afterDracula = findByTitle("Dracula")
    .flatMap(Library::nextBookBy);
System.out.println(afterDracula);
Optional[Book[title=Kidnapped, author=Robert Louis Stevenson, year=1886, genre=ADVENTURE]]
Optional.empty

Die Faustregel entspricht der bei Stream.flatMap(): Liefert die Funktion einen einfachen Wert, nimm map(); liefert sie ein Optional, nimm flatMap().

filter()

filter() behält den Wert, wenn er die Bedingung erfüllt; sonst ist das Ergebnis ein leeres Optional:

System.out.println(findByTitle("Treasure Island")
    .filter(book -> book.genre() == ADVENTURE));

System.out.println(findByTitle("Dracula")
    .filter(book -> book.genre() == ADVENTURE));
Optional[Book[title=Treasure Island, author=Robert Louis Stevenson, year=1883, genre=ADVENTURE]]
Optional.empty

„Dracula“ steht zwar in der Bibliothek, ist aber kein Abenteuerroman.

or()

or(), das es seit Java 9 gibt, liefert einen Fallback für ein leeres Optional – im Gegensatz zu orElseGet() keinen Wert, sondern ein weiteres Optional.

Im folgenden Beispiel weicht die Suche auf eine Suche aus, die Groß- und Kleinschreibung ignoriert:

static Optional<Book> findByTitleIgnoreCase(String title) {
  return BOOKS.stream()
      .filter(book -> book.title().equalsIgnoreCase(title))
      .findFirst();
}

Das folgende Listing sucht einmal mit or() und zum Vergleich mit orElseGet():

Optional<Book> withOr = findByTitle("treasure island")
    .or(() -> findByTitleIgnoreCase("treasure island"));
System.out.println(withOr);

Book withOrElseGet = findByTitle("treasure island")
    .orElseGet(() -> findByTitleIgnoreCase("treasure island").orElse(null));
System.out.println(withOrElseGet);
Optional[Book[title=Treasure Island, author=Robert Louis Stevenson, year=1883, genre=ADVENTURE]]
Book[title=Treasure Island, author=Robert Louis Stevenson, year=1883, genre=ADVENTURE]

findByTitle() findet nichts, weil die Bibliothek den Titel mit Großbuchstaben schreibt; findByTitleIgnoreCase() findet das Buch.

or() liefert wieder ein Optional<Book>, die Kette kann also weitergehen – z. B. mit map(Book::title). orElseGet() muss dagegen ein Book liefern: Der Supplier muss das Optional von findByTitleIgnoreCase() deshalb selbst auspacken, hier mit orElse(null) – und damit kann das Ergebnis wieder null sein.

Verketten statt verschachtelter null-Prüfungen

Was bringen dir diese Methoden? Im Folgenden wollen wir den Titel des nächsten Buchs vom Autor eines gegebenen Buchs suchen.

Mit Methoden, die null zurückgeben, musst du nach jedem Schritt eine null-Prüfung einbauen. Das folgende Listing verwendet dafür getByTitle() aus dem Abschnitt Was ist ein Optional in Java? und getNextBookBy(), das ebenfalls null statt eines leeren Optional zurückgibt:

static String nextTitleWithNullChecks(String title) {
  Book book = getByTitle(title);
  if (book != null) {
    Book next = getNextBookBy(book);
    if (next != null) {
      return next.title();
    }
  }
  return "(none)";
}

Mit Optional wird daraus eine gut lesbare Kette aus drei Schritten:

static String nextTitle(String title) {
  return findByTitle(title)
      .flatMap(Library::nextBookBy)
      .map(Book::title)
      .orElse("(none)");
}

Beide Methoden liefern für „Treasure Island“, „Dracula“ und „Ulysses“ dasselbe Ergebnis:

Treasure Island: Kidnapped / Kidnapped
Dracula: (none) / (none)
Ulysses: (none) / (none)

Die folgende Grafik zeigt die drei Titel auf ihrem Weg durch die Optional-Kette in nextTitle(): eine Spalte pro Titel, eine Zeile pro Schritt. Ein Optional mit Wert hat einen dunkelblauen Rahmen, ein leeres Optional einen grauen und der String, den orElse() liefert, einen hellblauen:

Drei Spalten für die Titel Treasure Island, Dracula und Ulysses und vier Zeilen für die Schritte findByTitle(title), .flatMap(Library::nextBookBy), .map(Book::title) und .orElse("(none)"). Treasure Island behält den ganzen Weg über einen Wert: Optional[Treasure Island], Optional[Kidnapped], Optional["Kidnapped"], "Kidnapped". Dracula wird bei flatMap() leer: Optional[Dracula], dann zweimal Optional.empty, dann "(none)". Ulysses ist von Anfang an leer: dreimal Optional.empty, dann "(none)"
Ist ein Optional einmal leer, reicht jeder weitere Schritt es unverändert weiter – bis orElse() den Fallback-Wert liefert

Die Optional-Kette enthält kein explizites if – jede aufgerufene Optional-Methode prüft selbst auf den fehlenden Wert. Die verschachtelte Variante hingegen braucht zwei null-Prüfungen, und jede, die du vergisst, wird zu einer NullPointerException; die Optional-Kette hat nichts, was du vergessen könntest.

Optional und Streams

Terminale Operationen, die ein Optional liefern

Fünf terminale Operationen eines Streams liefern ein Optional, weil der Stream leer sein kann: findFirst(), findAny(), min(), max() und reduce() ohne Startwert.

Der Artikel über Java Streams stellt sie vor; hier geht es darum, wie es mit dem Ergebnis weitergeht:

Optional<Book> oldest = BOOKS.stream()
    .min(Comparator.comparingInt(Book::year));
System.out.println(oldest.map(Book::title).orElseThrow());

Optional<Book> anyAfter1900 = BOOKS.stream()
    .filter(book -> book.year() > 1900)
    .findAny();
System.out.println(anyAfter1900);
Pride and Prejudice
Optional.empty

Bei oldest ist orElseThrow() berechtigt: Die Bibliothek ist nicht leer, also gibt es ein ältestes Buch. Ein Buch nach 1900 gibt es nicht, also ist anyAfter1900 leer.

Optional.stream()

Seit Java 9 macht Optional.stream() aus einem Optional einen Stream mit einem Element oder keinem. Zusammen mit flatMap() wird so aus einem Stream von Optionals ein Stream der vorhandenen Werte.

Das folgende Beispiel schlägt eine Wunschliste in der Bibliothek nach und behält diejenigen Bücher, die vorhanden sind:

List<String> wishList = List.of("Dracula", "Ulysses", "Moby-Dick", "Beloved");

List<Book> available = wishList.stream()
    .map(Library::findByTitle)
    .flatMap(Optional::stream)
    .toList();

available.forEach(book -> System.out.println(book.title()));
Dracula
Moby-Dick

map() erzeugt vier Optionals, zwei davon leer. Optional::stream macht aus jedem leeren Optional einen leeren Stream und aus jedem gefüllten einen Stream mit einem Buch. flatMap() führt diese Streams zu einem einzigen Stream zusammen – dem Stream derjenigen Bücher, die in den gefüllten Optionals lagen.

OptionalInt, OptionalLong und OptionalDouble

Primitive Streams liefern eigene Optional-Varianten: IntStream.max() z. B. ein OptionalInt, LongStream.max() ein OptionalLong und average() ein OptionalDouble.

Sie halten einen primitiven Wert, ohne ihn in ein Integer, Long oder Double zu verpacken, und du liest den Wert mit getAsInt(), getAsLong() oder getAsDouble():

OptionalInt newestYear = BOOKS.stream()
    .mapToInt(Book::year)
    .max();
System.out.println(newestYear);
System.out.println(newestYear.getAsInt());

OptionalDouble averageYear = BOOKS.stream()
    .filter(book -> book.year() > 1900)
    .mapToInt(Book::year)
    .average();
System.out.println(averageYear);
System.out.println(averageYear.orElse(Double.NaN));
OptionalInt[1898]
1898
OptionalDouble.empty
NaN

Die primitiven Varianten haben orElse(), orElseGet(), orElseThrow(), ifPresent(), ifPresentOrElse() und stream(), aber kein ofNullable(), map(), flatMap(), filter() oder or(). Willst du den Wert umformen, liest du ihn zuerst mit orElse() oder orElseThrow() aus.

Wann Optional – und wann nicht

Das Javadoc von Optional nennt den Zweck der Klasse selbst:

Optional is primarily intended for use as a method return type where there is a clear need to represent „no result,“ and where using null is likely to cause errors.

Auf Deutsch: Optional ist in erster Linie als Rückgabetyp einer Methode gedacht, wenn klar „kein Ergebnis“ ausgedrückt werden muss und null wahrscheinlich zu Fehlern führen würde.

Die folgenden Abschnitte zeigen, was daraus für Rückgabetypen, Felder, Parameter, Collections und deren Elemente folgt.

Rückgabetypen

Der Rückgabetyp ist der Platz für Optional: überall dort, wo eine Methode regulär kein Ergebnis haben kann, wie findByTitle() oder nextBookBy().

Eine Methode, die ein Optional liefert, darf selbst nie null zurückgeben. Das Javadoc sagt es ausdrücklich: Eine Variable vom Typ Optional „should never itself be null“, sollte also selbst nie null sein. Was sonst passiert, zeigt der Abschnitt Eine Optional-Methode, die null zurückgibt.

Felder

Innerhalb ihrer Klasse braucht ein Feld kein Optional: Die Klasse kennt ihr Feld und prüft es dort, wo sie es liest. Ich empfehle deshalb ein Feld, das null sein darf, und einen Getter, der Optional.ofNullable(field) zurückgibt – der aufrufende Code bekommt das Optional, und die Klasse behält das einfache Feld.

Dazu kommt ein zweiter Grund: Optional implementiert Serializable nicht.

Eine Klasse mit einem Optional-Feld lässt sich deshalb nicht serialisieren:

record Reservation(String title, Optional<String> note) implements Serializable {}
try (ObjectOutputStream out = new ObjectOutputStream(new ByteArrayOutputStream())) {
  out.writeObject(new Reservation("Dracula", Optional.of("second copy")));
}
java.io.NotSerializableException: java.util.Optional

Ein Feld, das null sein darf, hat dieses Problem nicht.

Methodenparameter

Angenommen, du willst die Bücher eines Autors suchen – einmal alle, einmal nur die eines bestimmten Genres.

Mit Optional als Parametertyp deckt eine einzige Methode beide Fälle ab:

static List<Book> booksBy(String author, Optional<Genre> genre) {
  return BOOKS.stream()
      .filter(book -> book.author().equals(author))
      .filter(book -> genre.map(g -> book.genre() == g).orElse(true))
      .toList();
}
booksBy("Jules Verne", Optional.empty());           // liefert 2 Bücher
booksBy("Jules Verne", Optional.of(ADVENTURE));     // liefert 1 Buch

Jeder Aufruf muss sein Argument verpacken, und nichts hindert den aufrufenden Code daran, null statt Optional.empty() zu übergeben.

Zwei separate Methoden sagen dasselbe direkter:

static List<Book> booksBy(String author) {
  return BOOKS.stream()
      .filter(book -> book.author().equals(author))
      .toList();
}

static List<Book> booksBy(String author, Genre genre) {
  return booksBy(author).stream()
      .filter(book -> book.genre() == genre)
      .toList();
}
booksBy("Jules Verne");                // liefert 2 Bücher
booksBy("Jules Verne", ADVENTURE);     // liefert 1 Buch

Ich empfehle die separaten Methoden. Bei mehr als zwei oder drei optionalen Parametern wächst ihre Zahl zu schnell – dann ist ein Builder oder ein Parameterobjekt die bessere Wahl.

Collections als Rückgabewert

Eine Methode, die eine Collection liefert, sagt „kein Ergebnis“ am besten mit einer leeren Collection:

static List<Book> booksPublishedIn(int year) {
  return BOOKS.stream()
      .filter(book -> book.year() == year)
      .toList();
}

booksPublishedIn(1865) liefert zwei Bücher, booksPublishedIn(1900) eine leere Liste. Ein Optional<List<Book>> hätte zwei Arten, „nichts“ zu sagen – ein leeres Optional und eine leere Liste –, und der aufrufende Code müsste beide behandeln.

Elemente in Collections

Auch in einer Collection sind Optionals fehl am Platz: Entweder enthält die Collection den Wert – oder sie enthält ihn nicht.

Das zeigt die Wunschliste aus dem Abschnitt Optional.stream(), diesmal ohne flatMap() gemappt:

List<Optional<Book>> results = wishList.stream()
    .map(Library::findByTitle)
    .toList();

results hat vier Elemente, zwei davon sind leere Optionals. Code, der die Liste liest, muss jedes Element erst auspacken und die leeren überspringen. Mit flatMap(Optional::stream) landen dagegen nur die beiden vorhandenen Bücher in der Liste.

Optional ist eine Value-based Class

Das Javadoc nennt Optional eine Value-based Class (wertbasierte Klasse): Zwei Optionals mit demselben Inhalt sind austauschbar, und es spielt keine Rolle, ob sie dasselbe Objekt sind. Das hat zwei Folgen für deinen Code.

Die erste Folge: Vergleiche Optionals mit equals(), nicht mit ==:

Optional<String> a = Optional.of("Dracula");
Optional<String> b = Optional.of("Dracula");

System.out.println(a.equals(b));
System.out.println(a == b);
true
false

equals() vergleicht den Inhalt, == die Objektidentität. Optional.of() erzeugt bei jedem Aufruf ein neues Objekt, also ist a == b false.

Die zweite Folge: Verwende ein Optional nicht zur Synchronisation.

Seit Java 16 warnt der javac-Compiler, wenn du auf einer Instanz einer Value-based Class synchronisierst:

void m(Optional<String> o) {
  synchronized (o) { }
}
warning: [identity] attempt to synchronize on an instance of a value-based class

Beide Regeln bereiten Optional auf Project Valhalla vor. Mit Java 28 wird JEP 401: Value Objects (Preview) kommen: Sind die Preview-Features aktiviert, wird Optional dann zu einer Value Class – einer Klasse, deren Objekte keine Identität haben. Für zwei Value-Objekte vergleicht == dann nicht mehr die Identität, sondern die Felder: Zwei Objekte sind unter == gleich, wenn ihre Felder unter == gleich sind.

Um zu zeigen, was das für Optional bedeutet, bekommt das Programm ein zweites Paar, c und d, deren Strings denselben Inhalt haben, aber zwei verschiedene Objekte sind:

Optional<String> c = Optional.of(new String("Dracula"));
Optional<String> d = Optional.of(new String("Dracula"));

System.out.println(c.equals(d));
System.out.println(c == d);

Auf einem JDK, das aus dem Valhalla-Repository gebaut ist, gibt das Programm mit --enable-preview für a, b, c und d aus:

true
true
true
false

a == b ist jetzt true, denn beide Optionals enthalten dasselbe String-Objekt – das Literal "Dracula" gibt es nur einmal. c == d bleibt false: String behält seine Identität, und die beiden neu erzeugten Strings sind verschiedene Objekte. Auch mit Valhalla ersetzt == also nicht equals().

synchronized auf einem Optional scheitert in Java 28 mit aktivierten Preview-Features zur Laufzeit:

java.lang.IdentityException: Cannot synchronize on an instance of value class java.util.Optional

Vergleicht dein Code Optionals mit equals() und synchronisiert nicht auf ihnen, verhält er sich mit und ohne Valhalla gleich.

Typische Fehler

isPresent() und get() statt map() und orElse()

Die direkteste Übersetzung einer null-Prüfung sieht so aus:

Optional<Book> book = findByTitle("Dracula");
String title;
if (book.isPresent()) {
  title = book.get().title();
} else {
  title = "(unknown)";
}

Dasselbe kannst du mit map() und orElse() deutlich kürzer schreiben:

String title = findByTitle("Dracula")
    .map(Book::title)
    .orElse("(unknown)");

Die erste Variante braucht eine Variable, eine Bedingung und zwei Zuweisungen, und sie verlässt sich darauf, dass get() nur nach isPresent() aufgerufen wird.

Ohne die Prüfung scheitert get() an einem leeren Optional mit der Exception, die du von orElseThrow() kennst:

java.util.NoSuchElementException: No value present

Die zweite Variante kann so nicht scheitern: Sie liest nie einen Wert, der nicht da ist.

Optional.of() für einen Wert, der null sein kann

Optional.of() wirft eine NullPointerException, wenn der übergebene Wert null ist; der Abschnitt Optional.ofNullable() zeigt den Fall mit Map.get(). Nimm stattdessen ofNullable() überall dort, wo der Wert aus einer API kommt, die null liefern kann.

Eine Optional-Methode, die null zurückgibt

Eine Methode mit dem Rückgabetyp Optional, die in einem Sonderfall null zurückgibt, lässt jeden Aufruf scheitern, der sich auf den Rückgabetyp verlässt:

static Optional<Book> findByTitleBroken(String title) {
  return title.isBlank() ? null : findByTitle(title);
}
findByTitleBroken(" ").ifPresent(System.out::println);
java.lang.NullPointerException: Cannot invoke "java.util.Optional.ifPresent(java.util.function.Consumer)" because the return value of "eu.happycoders.optional.Ch9Mistakes.findByTitleBroken(String)" is null

Der aufrufende Code hat alles richtig gemacht – und bekommt genau die Exception, die Optional verhindern sollte. Gib für den Sonderfall nicht null, sondern Optional.empty() zurück.

orElse() mit einem Fallback-Wert, der erst berechnet werden muss

orElse(fallbackTitle()) ruft fallbackTitle() auch dann auf, wenn das Optional einen Wert enthält und somit gar kein Fallback-Titel erforderlich ist; der Abschnitt orElse() und orElseGet() zeigt die Ausgabe. Für einen berechneten Fallback-Wert nimm orElseGet(() -> fallbackTitle()).

Optional nur, um ein if zu sparen

Optional ist kein Ersatz für jede null-Prüfung:

String author = "Mary Shelley";

Optional.ofNullable(author)
    .ifPresent(name -> System.out.println("Author: " + name));

Das Optional existiert hier für eine einzige Zeile und wird dann weggeworfen.

Das if sagt dasselbe direkt:

if (author != null) {
  System.out.println("Author: " + author);
}

Optional zeigt seine Stärke als Rückgabetyp einer Methode, nicht als lokales Hilfsmittel.

Die Optional-Methoden nach Java-Version

Die folgende Tabelle zeigt, in welcher Java-Version jede Methode von Optional dazugekommen ist:

Java-VersionMethoden
Java 8of(), ofNullable(), empty(), isPresent(), get(), ifPresent(), filter(), map(), flatMap(), orElse(), orElseGet(), orElseThrow(Supplier)
Java 9ifPresentOrElse(), or(), stream()
Java 10orElseThrow()
Java 11isEmpty()

Die primitiven Optional-Varianten OptionalInt, OptionalLong und OptionalDouble haben die Methoden, die mit Java 9, 10 und 11 dazukamen, in denselben Versionen bekommen – bis auf or(), das es für sie nicht gibt.

Seit Java 11 ist keine neue Methode dazugekommen. Was Project Valhalla ändert, sind nicht die Methoden von Optional, sondern seine Identität.

Zusammenfassung

Ein Optional enthält entweder genau einen Wert oder keinen. Als Rückgabetyp einer Methode sagt es in der Signatur, dass es kein Ergebnis geben kann, und zwingt den aufrufenden Code, zu entscheiden, was dann passiert. Du erzeugst es mit of(), ofNullable() oder empty(), holst den Wert mit orElse(), orElseGet() oder orElseThrow() heraus und formst es mit map(), flatMap(), filter() und or() um – map(), flatMap() und filter() arbeiten dabei nur dann, wenn ein Wert vorhanden ist, or() nur, wenn keiner vorhanden ist.

Meine Empfehlungen für den Alltag:

  • Nutze Optional als Rückgabetyp – nicht für Felder, Parameter, Collections oder deren Elemente.
  • Gib aus einer Methode mit dem Rückgabetyp Optional nie null zurück.
  • Bevorzuge map(), orElse() und orElseThrow() gegenüber dem Paar isPresent() und get().
  • Nimm orElseGet() statt orElse(), sobald der Fallback-Wert erst berechnet werden muss.
  • Vergleiche Optionals mit equals() und synchronisiere nie auf ihnen – dann verhält sich dein Code mit Valhalla gleich.

Woher Optionals am häufigsten kommen – aus der Stream-API –, ist Thema des Artikels über Java Streams; die Lambdas, die map(), filter() und orElseGet() entgegennehmen, erklärt der Artikel über Java Lambda-Ausdrücke.

Wenn dir der Artikel weitergeholfen hat, würde ich mich sehr über eine positive Bewertung auf meinem ProvenExpert-Profil freuen. Dein Feedback hilft mir, meine Inhalte weiter zu verbessern und motiviert mich, neue informative Artikel zu schreiben.

👉 Bewertung abgeben

Du möchtest über alle neuen Java-Features auf dem Laufenden sein? Dann klicke hier, um dich für den HappyCoders-Newsletter anzumelden.

👉 Newsletter-Anmeldung

Dieses Thema im eigenen Code?

Du hast den Artikel gelesen – im Training arbeitet ihr damit. Einen Tag lang gehen wir die Themen an euren eigenen Projekten durch, statt an konstruierten Beispielen.

Praxisnah, verständlich und direkt auf euren Projektalltag übertragbar. Statt Theorie vermittle ich Prinzipien, die euch helfen, Code langfristig besser, wartbarer und performanter zu schreiben.

Java Streams BasicsAlle Trainings ansehen

Werde ein:e bessere:r Java-Entwickler:in

Mit meinem kostenlosen Newsletter bleibst du vorn. Modernes Java: neue Versionen & Features, Performance und JVM-Insights – 1x im Monat.

Suche