Eine fehlende Optional-Methode und ein Workaround


Ein wirklich hässliches Pattern, um mehrere Dinge auszuprobieren, bis eines davon klappt, ist:

int x = ...
int y = ...
int z = ...

Integer result = null;
if (x % 2 == 0) {
    result = x / 2;
}
if (result == null && y % 2 == 0) {
    result = y / 2;
}
if (result == null && z % 2 == 0) {
    result = z / 2;
}
String s = result == null
    ? "all numbers were odd"
    : "the result is " + result;

Eigentlich sollte Optional uns in diesen Fällen die Null-Checks abnehmen. In Scala und mit der Optional-Variante von Guava geht das auch sehr schön, da in beiden Fällen Methoden vorhanden sind, um einen vorhandenen Wert unverändert „durchzuschleusen“, aber im „leeren“ Fall stattdessen ein anderes Optional zu verwenden.

Unverständlicherweise fehlt eine solche Funktion in Java 8. Die direkte Übersetzung unseres Beispiels sieht deshalb auch nicht viel hübscher als das Original aus:

public static Optional<Integer> half(int x) {
    return x % 2 == 0 
        ? Optional.of(x / 2) 
        : Optional.empty();
}

...

Optional<Integer> result = half(x);
if (! result.isPresent()) {
    result = half(y);
}
if (! result.isPresent()) {
    result = half(z);
}
String s = result
    .map(n -> "the result is " + n)
    .orElse("all numbers were odd");

Zum Vergleich die elegante Scala-Version:

def half(n : Int) : Option[Int] = 
    if (n % 2 == 0) Some(n / 2) else None

val s = half(x)
    .orElse(half(y))
    .orElse(half(z))
    .map(n => "the result is " + n)
    .getOrElse("all numbers were odd")

Haben wir eine Chance, das wenigstens ungefähr so nachzubauen? Ja, es gibt einen Workaround. Das ist wieder eines jener Probleme, über das man durchaus eine Weile grübeln kann, aber wenn man einmal die Lösung kennt, erscheint sie ganz selbstverständlich:

String s = half(x).map(Optional::of)
    .orElse(half(y)).map(Optional::of)
    .orElse(half(z))
    .map(n -> "the result is " + n)
    .orElse("all numbers were odd");

Der Trick ist, mit einem map aus dem Optional<Integer> jeweils ein Optional<Optional<Integer>> zu machen, bevor wir mit der orElse-Methode wieder eine Optional-Ebene „abbauen“. Insgesamt macht diese map-orElse-Kombination also genau dasselbe, was in Scala ein einzelnes orElse (oder im Optional von Guava ein or) erledigt.

Wir haben sogar die Chance, hier noch etwas besser zu machen. Was wäre, wenn die half-Aufrufe lange dauern würden oder ungewünschte Seiteneffekte hätten? Dann sollten die Aufrufe natürlich lazy erfolgen, und das geht, in dem man das orElse einfach durch orElseGet ersetzt:

String s = half(x).map(Optional::of)
    .orElseGet(() -> half(y)).map(Optional::of)
    .orElseGet(() -> half(z))
    .map(n -> "the result is " + n)
    .orElse("all numbers were odd");

Trotzdem hoffe ich sehr, dass die Verantwortlichen ein Einsehen haben, Optional in Java 9 ein wenig komfortabler gestalten, und uns damit solche Verrenkungen in Zukunft ersparen.

Advertisements

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s