Optional in Java 8

Das Behandeln von Null-Werten kann in Java sehr lästig sein. Zu leichtsinniger Einsatz der Null-Referenz hat unangenehme Laufzeitfehler zur Folge. Aber es ist Besserung in Sicht: mit Java 8 hielt nun endlich die Klasse Optional Einzug in die Klassenbibliothek. Bisher konnte man nur durch das Einbinden von externen Bibliotheken (z. B. Guava) mit Optional-Typen arbeiten.

Natürlich werden damit nicht die Null-Referenz an sich und die aufwändig zu vermeidenden NullPointerExceptions abgeschafft. Das ginge aus Gründen der Abwärtskompatibilität schon gar nicht. Aber die Klasse Optional kann zumindest in Zukunft helfen, Null-Werte einfacher zu handhaben. So lässt sich nun die Absicht verdeutlichen, dass Werte leer sein dürfen. Gerade für Schnittstellen und APIs ist es unerlässlich, bei Rückgabewerten und Methodenparametern den Fall, dass nichts zurückkommt bzw. übergeben wird, kenntlich zu machen. Uneinheitliche Implementierungen, zum Beispiel im Collection Framework, haben uns Java-Entwicklern in der Vergangenheit das Leben erschwert.

Eine Instanz von Optional ist nichts weiter als eine Referenz auf das eigentliche Objekt. Da man diese für die Verwendung erst auspacken muss, macht man sich aber eher Gedanken darüber, dass sie möglicherweise auch null sein kann.

Erzeugt werden Instanzen von Optional mit statischen Fabrikmethoden:

Optional<Integer> number = Optional.of(1234);

try {
	Optional.of(null);
} catch (NullPointerException e) {
	e.printStackTrace();
}

Optional<Object> maybeNull = Optional.ofNullable(null);
Optional<Object> containsNull = Optional.empty();

Mit of() wird zum Erzeugungszeitpunkt ggf. sofort eine Exception geworfen. Damit merkt man frühzeitig, wenn eine Referenz wider Erwarten null ist, und das erleichtert die spätere Fehlersuche. Man kann natürlich auch explizit eine leere Optional-Instanz (empty()) oder eine Instanz, die leer sein darf (ofNullable()), erzeugen.

Will man den inneren Wert einer Optional-Instanz ermitteln, kann man zunächst das Vorhandensein abfragen (isPresent()) und dann mit get() das referenzierte Objekt hervorholen. Auch hier werden aber Fehler geworfen, wenn man auf den Inhalt einer leeren Optional-Instanz zugreifen möchte.

System.out.println(String.format("Contains number?  %s", number.isPresent()));
System.out.println(String.format("Number: %s", number.get()));

System.out.println(String.format("Contains value?  %s", maybeNull.isPresent()));
try {
	maybeNull.get();
} catch (NoSuchElementException e) {
	e.printStackTrace();
}

Möchte man auf möglicherweise leere Optional-Instanzen reagieren, kann man alternative Werte zurückgeben. Diese können entweder konstant sein (orElse()) oder über einen Supplier erzeugt werden (orElseGet()), wobei der Supplier einfach als ein Lambda-Ausdruck übergeben werden kann. Außerdem hat man die Möglichkeit, eine benutzerdefinierte Exception zu werfen (orElseThrow()).

System.out.println(maybeNull.orElse("alternative value"));
System.out.println(containsNull.orElseGet(() -> 123));
try {
	containsNull.orElseThrow(() -> new RuntimeException());
} catch (RuntimeException e) {
	e.printStackTrace();
}

Optional ist natürlich typisiert und gibt immer den Typ zurück, den man bei der Erzeugung angegeben hat. Lästige Castings und Typprüfungen entfallen dadurch, und der Compiler hilft bei falschen Typzuweisungen.

Die weiteren Neuerungen von Java 8 (Lambda, Streams, …) spielen auch wunderbar mit Optional zusammen. Ein Beispiel dazu haben wir weiter oben mit orElseGet() schon gesehen. Oder man kann mit ifPresent() etwas tun, wenn in Optional ein Objekt präsent, also nicht leer ist.

containsNull.ifPresent(System.out::println);
number.ifPresent(System.out::println);

Optional bietet zudem eine filter() Methode, die null-safe ist. Wenn das Optional leer ist oder das Prädikat zu false evaluiert, wird ein leeres Optional zurückgegeben, ansonsten der Wert wieder als Optional verpackt.

System.out.println(number.filter(it -> it > 100));
System.out.println(maybeNull.filter(it -> it instanceof Object));

Aus Listen mit Optional-Elementen können per filter() die nicht leeren Objekte ermittelt werden, um sie über die Stream-API weiter zu verarbeiten.

Arrays.asList(number, maybeNull, containsNull)
  .stream()
  .filter(it -> it.isPresent())
  .forEach(System.out::println);

Zum Abschluss wollen wir uns nochmal ein etwas komplexeres Beispiel anschauen. Nehmen wir an, wir haben ein Bank-Objekt, von dem wir uns einen Kunden laden können, der wiederum ein Konto hat, dessen Kontostand wir erfahren möchten. Im einfachsten Fall würden wir das folgendermaßen lösen:

System.out.println(bank.findCustomer(1L).getAccount().getBalance());

Bei dieser Anweisung kann einiges schief gehen. Auch wenn wir davon ausgehen, dass bank nicht null sein kann und getBalance() auch immer eine Zahl zurückliefert, so könnte aber findCustomer() oder auch getAccount() jeweils null liefern, wenn nämlich kein Kunde mit der ID gefunden werden konnte oder der Kunde kein Konto besitzt. Darum müsste man eher folgende Kaskade von if-Statements implementieren:

BigDecimal balance = BigDecimal.ZERO;
if (bank != null) {
	if (bank.findCustomer(1L) != null) {
		if (bank.findCustomer(1L).getAccount() != null) {
			balance = bank.findCustomer(1L).getAccount().getBalance();
		}
	}
}
System.out.println(balance);

Die alternative JVM-Sprache Groovy bringt schon lange als Erleichterung den sicheren Dereferentiator (“?.”), der in Zusammenarbeit mit dem Elvis-Operator (“?:”) das if-Statement auf eine Zeile schrumpfen lässt:

println bank?.findCustomer(1L)?.account?.balance ?: 0

In Java 8 können wir es zwar nicht ganz so elegant, aber immerhin auch auf einer Zeile lösen, wenn findCustomer() und getAccount() jeweils eine Optional-Instanz zurückliefern:

System.out.println(bank.findCustomer(1L)
  .flatMap(Customer::getAccount)
  .map(Account::getBalance)
  .orElse(BigDecimal.ZERO));

Die Methode flatMap() packt aus dem Optional (Rückgabewert von findCustomer()) den eigentlichen Customer aus und fragt mit getAccount() (über Lambda-Ausdruck bzw. Methodenreferenz) das Konto des Kunden ab. Das Konto wird dann wieder in ein Optional eingepackt und über map() kann man nun auf den Kontostand zugreifen. Das besondere ist, dass flatMap() und map() null-safe sind, dass heißt sie funktionieren auch mit einem leeren Optional. Ist am Ende der Anweisung in Optional noch ein Wert enthalten, wird dieser, andernfalls ein Alternativwert ausgegeben (orElse()).

Die Methoden flatMap() und map() machen übrigens nichts anderes, als die übergebene Funktion (Lambdaausdruck) auszuführen und den Inhalt von Optional als Parameter zu übergeben. Das Ergebnis wird dann wieder in ein Optional gepackt. Die Methode flatMap() schachtelt aber keine Optional-Objekte sondern flacht die Hierarchie ab. Stattdessen würde map() ein Optional von einem Optional von einem Objekt liefern.

// Optional[Optional[de.oio.optional.Account@5b2133b1]]
System.out.println(bank2.findCustomer(1L).map(Customer::getAccount));

// Optional[de.oio.optional.Account@5b2133b1]
System.out.println(bank2.findCustomer(1L).flatMap(Customer::getAccount));
Short URL for this post: http://wp.me/p4nxik-2bd
This entry was posted in Java and Quality, Java Basics and tagged , , . Bookmark the permalink.

Leave a Reply