Optional orElse Optional in Java

137

Ich habe mit dem neuen optionalen Typ in Java 8 gearbeitet und bin auf eine allgemeine Operation gestoßen, die funktional nicht unterstützt wird: eine "orElseOptional"

Betrachten Sie das folgende Muster:

Optional<Result> resultFromServiceA = serviceA(args);
if (resultFromServiceA.isPresent) return result;
else {
    Optional<Result> resultFromServiceB = serviceB(args);
    if (resultFromServiceB.isPresent) return resultFromServiceB;
    else return serviceC(args);
}

Es gibt viele Formen dieses Musters, aber es läuft darauf hinaus, ein "orElse" für ein optionales Element zu wollen, das eine Funktion übernimmt, die ein neues optionales Element erzeugt, das nur aufgerufen wird, wenn das aktuelle nicht vorhanden ist.

Die Implementierung würde folgendermaßen aussehen:

public Optional<T> orElse(Supplier<Optional<? extends T>> otherSupplier) {
    return value != null ? this : other.get();
}

Ich bin gespannt, ob es einen Grund gibt, warum es eine solche Methode nicht gibt, ob ich Optional nur unbeabsichtigt verwende und welche anderen Möglichkeiten die Leute gefunden haben, um mit diesem Fall umzugehen.

Ich sollte sagen, dass ich denke, dass Lösungen mit benutzerdefinierten Dienstprogrammklassen / -methoden nicht elegant sind, da Leute, die mit meinem Code arbeiten, nicht unbedingt wissen, dass sie existieren.

Wenn jemand weiß, wird eine solche Methode in JDK 9 enthalten sein, und wo könnte ich eine solche Methode vorschlagen? Dies scheint mir eine ziemlich krasse Auslassung der API zu sein.

Yona Appletree
quelle
13
Siehe dieses Problem . Zur Verdeutlichung: Dies wird bereits in Java 9 sein - wenn nicht in einem zukünftigen Update von Java 8.
Obicere
Es ist! Danke, habe das bei meiner Suche nicht gefunden.
Yona Appletree
2
@Obicere Dieses Problem tritt hier nicht auf, da es sich um ein Verhalten bei leerem Optional handelt, nicht um ein alternatives Ergebnis . Optional hat bereits orElseGet()für was OP benötigt, nur generiert es keine schöne kaskadierende Syntax.
Marko Topolnik
1
Tolle Tutorials zu Java Optional: codeflex.co/java-optional-no-more-nullpointerexception
John Detroit

Antworten:

86

Dies ist Teil von JDK 9 in Form von or, das a Supplier<Optional<T>>. Ihr Beispiel wäre dann:

return serviceA(args)
    .or(() -> serviceB(args))
    .or(() -> serviceC(args));

Für Details siehe den Javadoc oder diesen Beitrag, den ich geschrieben habe.

Nicolai
quelle
Nett. Dieser Zusatz muss ein Jahr alt sein und ich habe es nicht bemerkt. In Bezug auf die Frage in Ihrem Blog würde das Ändern des Rückgabetyps die Binärkompatibilität beeinträchtigen, da Anweisungen zum Aufrufen des Bytecodes auf die vollständige Signatur einschließlich des Rückgabetyps verweisen, sodass keine Möglichkeit besteht, den Rückgabetyp von zu ändern ifPresent. Aber ich denke, der Name ifPresentist sowieso nicht gut. Für alle anderen Methoden nicht „anders“ im Namen tragen (wie map, filter, flatMap), wird angedeutet , dass sie nichts tun , wenn kein Wert vorhanden ist, so sollte , warum ifPresent...
Holger
Fügen Sie also eine Optional<T> perform(Consumer<T> c)Methode hinzu, um eine Verkettung zu ermöglichen perform(x).orElseDo(y)( orElseDoals Alternative zu Ihrer vorgeschlagenen Methode, um im Namen aller Methoden ifEmptykonsistent zu sein else, die möglicherweise etwas für fehlende Werte tun). Sie können dies performin Java 9 nachahmen, stream().peek(x).findFirst()obwohl dies ein Missbrauch der API ist und es immer noch keine Möglichkeit gibt, eine auszuführen, Runnableohne Consumergleichzeitig eine anzugeben …
Holger
65

Der sauberste Ansatz für "Try Services" angesichts der aktuellen API wäre:

Optional<Result> o = Stream.<Supplier<Optional<Result>>>of(
    ()->serviceA(args), 
    ()->serviceB(args), 
    ()->serviceC(args), 
    ()->serviceD(args))
.map(Supplier::get)
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst();

Der wichtige Aspekt ist nicht die (konstante) Kette von Operationen, die Sie einmal schreiben müssen, sondern wie einfach es ist, einen anderen Dienst hinzuzufügen (oder die Liste der Dienste zu ändern, ist allgemein). Hier reicht es aus, eine einzelne hinzuzufügen oder zu entfernen ()->serviceX(args).

Aufgrund der verzögerten Auswertung von Streams wird kein Dienst aufgerufen, wenn ein vorhergehender Dienst einen nicht leeren zurückgegeben hat Optional.

Holger
quelle
13
Ich habe das gerade in einem Projekt verwendet, Gott sei Dank machen wir aber keine Code-Reviews.
Ilya Smagin
3
Es ist viel sauberer als eine Kette von "orElseGet", aber es ist auch viel schwieriger zu lesen.
Slartidan
4
Das ist sicherlich richtig ... aber ehrlich gesagt brauche ich eine Sekunde, um es zu analysieren, und ich komme nicht mit dem Vertrauen davon, dass es richtig ist. Wohlgemerkt, mir ist klar, dass dies ein Beispiel ist, aber ich könnte mir eine kleine Änderung vorstellen, die dazu führen würde, dass es nicht träge bewertet wird oder einen anderen Fehler aufweist, der auf einen Blick nicht zu unterscheiden wäre. Für mich fällt dies in die Kategorie "wäre als Dienstprogramm anständig".
Yona Appletree
3
Ich frage mich, ob das Umschalten .map(Optional::get)mit .findFirst()das "Lesen" erleichtert, z. B. .filter(Optional::isPresent).findFirst().map(Optional::get)"Lesen" wie "Finden Sie das erste Element im Stream, für das Optional :: isPresent wahr ist, und reduzieren Sie es dann durch Anwenden von Optional :: get".
Schatten
3
Komisch, ich habe vor ein paar Monaten eine sehr ähnliche Lösung für eine ähnliche Frage gepostet . Dies ist das erste Mal, dass ich darauf stoße.
Shmosel
34

Es ist nicht schön, aber das wird funktionieren:

return serviceA(args)
  .map(Optional::of).orElseGet(() -> serviceB(args))
  .map(Optional::of).orElseGet(() -> serviceC(args))
  .map(Optional::of).orElseGet(() -> serviceD(args));

.map(func).orElseGet(sup)ist ein ziemlich praktisches Muster für die Verwendung mit Optional. Es bedeutet "Wenn dies OptionalWert enthält v, gib mir func(v), sonst gib mir sup.get()".

In diesem Fall rufen wir an serviceA(args)und bekommen eine Optional<Result>. Wenn das OptionalWert enthält v, wollen wir bekommen Optional.of(v), aber wenn es leer ist, wollen wir bekommen serviceB(args). Spülen-Wiederholen mit mehr Alternativen.

Andere Verwendungen dieses Musters sind

  • .map(Stream::of).orElseGet(Stream::empty)
  • .map(Collections::singleton).orElseGet(Collections::emptySet)
Mischa
quelle
1
Wenn ich diese Strategie verwende, sagt Eclipse: "Die Methode orElseGet (Supplier <? erweitert String>) vom Typ Optional <String> ist für die Argumente (() -> {}) nicht anwendbar." Es scheint nicht zu erwägen, eine optionale Strategie als gültige Strategie für den String zurückzugeben?
Chrismarx
@chrismarx () -> {}gibt kein zurück Optional. Was versuchst du zu erreichen?
Mischa
1
Ich versuche nur, dem Beispiel zu folgen. Das Verketten der Kartenaufrufe funktioniert nicht. Meine Dienste geben Zeichenfolgen zurück, und dann ist natürlich keine .map () -Option verfügbar
chrismarx
1
Hervorragende lesbare Alternative zum kommenden or(Supplier<Optional<T>>)Java 9
David M.
1
@ Sheepy du liegst falsch. .map()auf einem leeren Optionalwird ein leeres erzeugen Optional.
Mischa
27

Vielleicht ist es das, wonach Sie suchen: Holen Sie sich den Wert von der einen oder anderen Option

Andernfalls möchten Sie vielleicht einen Blick darauf werfen Optional.orElseGet. Hier ist ein Beispiel dafür, was Sie meiner Meinung nach suchen:

result = Optional.ofNullable(serviceA().orElseGet(
                                 () -> serviceB().orElseGet(
                                     () -> serviceC().orElse(null))));
aioobe
quelle
2
Dies ist, was Genies normalerweise tun. Bewerten Sie das Optionale als nullbar und das Umschließen mit ofNullableist das Coolste, was ich je gesehen habe.
Jin Kwon
5

Angenommen, Sie arbeiten noch mit JDK8, gibt es mehrere Optionen.

Option 1: Erstellen Sie Ihre eigene Hilfsmethode

Z.B:

public class Optionals {
    static <T> Optional<T> or(Supplier<Optional<T>>... optionals) {
        return Arrays.stream(optionals)
                .map(Supplier::get)
                .filter(Optional::isPresent)
                .findFirst()
                .orElseGet(Optional::empty);
    }
}

Damit Sie tun können:

return Optionals.or(
   ()-> serviceA(args),
   ()-> serviceB(args),
   ()-> serviceC(args),
   ()-> serviceD(args)
);

Option 2: Verwenden Sie eine Bibliothek

Zum Beispiel unterstützt Google Guavas Optional einen ordnungsgemäßen or()Betrieb (genau wie JDK9), z.

return serviceA(args)
  .or(() -> serviceB(args))
  .or(() -> serviceC(args))
  .or(() -> serviceD(args));

(Wo jeder der Dienste zurückkehrt com.google.common.base.Optional, anstatt java.util.Optional).

Schafig
quelle
Ich habe nicht Optional<T>.or(Supplier<Optional<T>>)in den Guava-Dokumenten gefunden. Hast du einen Link dafür?
Tamas Hegedus
.orElseGet gibt T zurück, aber Optional :: empty gibt Optional <T> zurück. Optional.ofNullable (........ orElse (null)) hat den gewünschten Effekt, wie von @aioobe beschrieben.
Miguel Pereira
@TamasHegedus Du meinst in Option # 1? Es ist eine benutzerdefinierte Implementierung, die sich darüber befindet. ps: Entschuldigung für die späte Antwort
Sheepy
@ MiguelPereira .orElseGet gibt in diesem Fall Optional <T> zurück, da es mit Optional <Optional <T>>
Sheepy
2

Dies scheint eine gute Lösung für den Mustervergleich und eine traditionellere Optionsoberfläche mit einigen und keiner Implementierungen (wie die in Javaslang , FunctionalJava ) oder eine faule Vielleicht- Implementierung in Cyclops- React zu sein. Ich bin der Autor dieser Bibliothek.

Mit cyclops reagieren Sie auch strukturelle verwenden können Pattern - Matching auf JDK - Typen. Optional können Sie aktuelle und fehlende Fälle über das Besuchermuster abgleichen . es würde ungefähr so ​​aussehen -

  import static com.aol.cyclops.Matchables.optional;

  optional(serviceA(args)).visit(some -> some , 
                                 () -> optional(serviceB(args)).visit(some -> some,
                                                                      () -> serviceC(args)));
John McClean
quelle