Optionale Verwendung von Java 8 mit Stream :: flatMap


239

Das neue Java 8-Stream-Framework und seine Freunde sorgen für einen sehr präzisen Java-Code, aber ich bin auf eine scheinbar einfache Situation gestoßen, die schwierig zu formulieren ist.

Betrachten Sie eine List<Thing> thingsund Methode Optional<Other> resolve(Thing thing). Ich möchte das Things auf Optional<Other>s abbilden und das erste bekommen Other. Die naheliegende Lösung wäre die Verwendung things.stream().flatMap(this::resolve).findFirst(), flatMaperfordert jedoch , dass Sie einen Stream zurückgeben und Optionalkeine stream()Methode haben (oder ist es eine Collectionoder eine Methode, mit der Sie ihn konvertieren oder als anzeigen können Collection).

Das Beste, was ich mir einfallen lassen kann, ist Folgendes:

things.stream()
    .map(this::resolve)
    .filter(Optional::isPresent)
    .map(Optional::get)
    .findFirst();

Aber das scheint für einen sehr häufigen Fall furchtbar langwierig zu sein. Hat jemand eine bessere Idee?


Nachdem ich ein wenig mit Ihrem Beispiel codiert habe, finde ich die explizite Version besser lesbar als die .flatMap(Optional::toStream), die sich auf Ihre Version bezieht, wenn sie existiert hat. Sie sehen tatsächlich, was los ist.
Skiwi

19
@skiwi Nun, Optional.streamexistiert jetzt in JDK 9 ....
Stuart Marks

Ich bin gespannt, wo dies dokumentiert ist und wie der Prozess war, um es zu bekommen. Es gibt einige andere Methoden, die wirklich so scheinen, als ob sie existieren sollten, und ich bin gespannt, wo Diskussionen über API-Änderungen stattfinden.
Yona Appletree


10
Das Lustige ist, dass JDK-8050820 in seiner Beschreibung tatsächlich auf diese Frage verweist!
Didier L

Antworten:


263

Java 9

Optional.stream wurde zu JDK 9 hinzugefügt. Auf diese Weise können Sie Folgendes ausführen, ohne dass eine Hilfsmethode erforderlich ist:

Optional<Other> result =
    things.stream()
          .map(this::resolve)
          .flatMap(Optional::stream)
          .findFirst();

Java 8

Ja, dies war eine kleine Lücke in der API, da es etwas unpraktisch ist, aus einem optionalen Stream einen Stream mit einer Länge von null oder eins zu machen. Sie könnten dies tun:

Optional<Other> result =
    things.stream()
          .map(this::resolve)
          .flatMap(o -> o.isPresent() ? Stream.of(o.get()) : Stream.empty())
          .findFirst();

Es ist jedoch etwas umständlich, den ternären Operator in der flatMap zu haben. Daher ist es möglicherweise besser, eine kleine Hilfsfunktion zu schreiben, um dies zu tun:

/**
 * Turns an Optional<T> into a Stream<T> of length zero or one depending upon
 * whether a value is present.
 */
static <T> Stream<T> streamopt(Optional<T> opt) {
    if (opt.isPresent())
        return Stream.of(opt.get());
    else
        return Stream.empty();
}

Optional<Other> result =
    things.stream()
          .flatMap(t -> streamopt(resolve(t)))
          .findFirst();

Hier habe ich den Aufruf zum Auflösen () eingefügt, anstatt eine separate map () -Operation zu haben, aber dies ist Geschmackssache.


2
Ich denke nicht, dass sich die API bis Java 9 jetzt ändern kann.
Assylias

5
@ Hypher Danke. Die .filter (). Map () -Technik ist nicht schlecht und vermeidet Abhängigkeiten von Hilfsmethoden. »Es wäre schön, wenn es einen prägnanteren Weg gäbe. Ich werde untersuchen, ob Optional.stream () hinzugefügt wird.
Stuart Marks

43
Ich bevorzuge:static <T> Stream<T> streamopt(Optional<T> opt) { return opt.map(Stream::of).orElse(Stream.empty()); }
kubek2k

5
Ich wünschte, sie würden nur eine OptionalÜberladung hinzufügen Stream#flatMap... auf diese Weise könnten Sie einfach schreibenstream().flatMap(this::resolve)
Flocken

4
@flkes Ja, wir haben diese Idee umgangen, aber sie scheint jetzt, wo es sie gibt (in JDK 9), nicht mehr so ​​viel Wert hinzuzufügen Optional.stream().
Stuart Marks

69

Ich füge diese zweite Antwort basierend auf einer vorgeschlagenen Bearbeitung durch den Benutzer srborlongan zu meiner anderen Antwort hinzu . Ich denke, die vorgeschlagene Technik war interessant, aber sie war nicht wirklich als Bearbeitung für meine Antwort geeignet. Andere stimmten zu und die vorgeschlagene Änderung wurde abgelehnt. (Ich war nicht einer der Wähler.) Die Technik hat jedoch ihre Berechtigung. Es wäre am besten gewesen, wenn Srborlongan seine eigene Antwort gepostet hätte. Dies ist noch nicht geschehen, und ich wollte nicht, dass die Technik im Nebel des von StackOverflow abgelehnten Bearbeitungsverlaufs verloren geht. Deshalb habe ich beschlossen, sie selbst als separate Antwort anzuzeigen.

Grundsätzlich besteht die Technik darin, einige der OptionalMethoden auf clevere Weise zu verwenden, um zu vermeiden, dass ein ternärer Operator ( ? :) oder eine if / else-Anweisung verwendet werden muss.

Mein Inline-Beispiel würde folgendermaßen umgeschrieben:

Optional<Other> result =
    things.stream()
          .map(this::resolve)
          .flatMap(o -> o.map(Stream::of).orElseGet(Stream::empty))
          .findFirst();

Ein Beispiel, das eine Hilfsmethode verwendet, wird folgendermaßen umgeschrieben:

/**
 * Turns an Optional<T> into a Stream<T> of length zero or one depending upon
 * whether a value is present.
 */
static <T> Stream<T> streamopt(Optional<T> opt) {
    return opt.map(Stream::of)
              .orElseGet(Stream::empty);
}

Optional<Other> result =
    things.stream()
          .flatMap(t -> streamopt(resolve(t)))
          .findFirst();

KOMMENTAR

Vergleichen wir die Originalversion mit der modifizierten Version direkt:

// original
.flatMap(o -> o.isPresent() ? Stream.of(o.get()) : Stream.empty())

// modified
.flatMap(o -> o.map(Stream::of).orElseGet(Stream::empty))

Das Original ist ein unkomplizierter, wenn auch fachmännischer Ansatz: Wir bekommen einen Optional<Other>; Wenn es einen Wert hat, geben wir einen Stream zurück, der diesen Wert enthält, und wenn es keinen Wert hat, geben wir einen leeren Stream zurück. Ziemlich einfach und leicht zu erklären.

Die Modifikation ist clever und hat den Vorteil, dass sie Bedingungen vermeidet. (Ich weiß, dass einige Leute den ternären Operator nicht mögen. Wenn er missbraucht wird, kann dies tatsächlich dazu führen, dass Code schwer zu verstehen ist.) Manchmal können die Dinge jedoch zu klug sein. Der geänderte Code beginnt ebenfalls mit einem Optional<Other>. Dann ruft es auf, Optional.mapwas wie folgt definiert ist:

Wenn ein Wert vorhanden ist, wenden Sie die bereitgestellte Zuordnungsfunktion darauf an. Wenn das Ergebnis nicht null ist, geben Sie eine Option zurück, die das Ergebnis beschreibt. Andernfalls geben Sie eine leere Option zurück.

Der map(Stream::of)Anruf gibt eine zurück Optional<Stream<Other>>. Wenn in der Eingabe Optional ein Wert vorhanden war, enthält die zurückgegebene Option einen Stream, der das einzelne Ergebnis Other enthält. Wenn der Wert jedoch nicht vorhanden war, ist das Ergebnis leer. Optional.

Als nächstes gibt der Aufruf von orElseGet(Stream::empty)einen Wert vom Typ zurück Stream<Other>. Wenn sein Eingabewert vorhanden ist, erhält er den Wert, der das Einzelelement ist Stream<Other>. Andernfalls (wenn der Eingabewert fehlt) wird ein Leerzeichen zurückgegeben Stream<Other>. Das Ergebnis ist also korrekt, genauso wie der ursprüngliche Bedingungscode.

In den Kommentaren zu meiner Antwort bezüglich der abgelehnten Bearbeitung hatte ich diese Technik als "prägnanter, aber auch dunkler" beschrieben. Ich stehe dazu. Ich brauchte eine Weile, um herauszufinden, was es tat, und ich brauchte auch eine Weile, um die obige Beschreibung dessen, was es tat, aufzuschreiben. Die Schlüssel-Subtilität ist die Transformation von Optional<Other>nach Optional<Stream<Other>>. Sobald Sie dies verstanden haben, macht es Sinn, aber es war mir nicht klar.

Ich werde jedoch anerkennen, dass Dinge, die anfangs dunkel sind, mit der Zeit idiomatisch werden können. Es könnte sein, dass diese Technik der beste Weg in der Praxis ist, zumindest bis sie Optional.streamhinzugefügt wird (falls dies jemals der Fall sein sollte).

UPDATE: Optional.stream wurde zu JDK 9 hinzugefügt.


16

Sie können es nicht prägnanter machen, als Sie es bereits tun.

Sie behaupten, dass Sie nicht wollen .filter(Optional::isPresent) und .map(Optional::get) .

Dies wurde durch die von @StuartMarks beschriebene Methode behoben. Als Ergebnis ordnen Sie es jetzt einem zu Optional<T>, sodass Sie jetzt .flatMap(this::streamopt)und get()am Ende ein verwenden müssen.

Es besteht also immer noch aus zwei Anweisungen und Sie können jetzt Ausnahmen mit der neuen Methode erhalten! Denn was ist, wenn jedes optionale leer ist? Dann findFirst()wird der eine leere Option zurückgeben und Ihr get()wird fehlschlagen!

Also was hast du:

things.stream()
    .map(this::resolve)
    .filter(Optional::isPresent)
    .map(Optional::get)
    .findFirst();

ist eigentlich der beste Weg, um das zu erreichen, was Sie wollen, und das heißt, Sie möchten das Ergebnis als T, nicht als speichern Optional<T>.

Ich habe mir erlaubt, eine CustomOptional<T>Klasse zu erstellen , die das einschließt Optional<T>und eine zusätzliche Methode bietet flatStream(). Beachten Sie, dass Sie nicht erweitern können Optional<T>:

class CustomOptional<T> {
    private final Optional<T> optional;

    private CustomOptional() {
        this.optional = Optional.empty();
    }

    private CustomOptional(final T value) {
        this.optional = Optional.of(value);
    }

    private CustomOptional(final Optional<T> optional) {
        this.optional = optional;
    }

    public Optional<T> getOptional() {
        return optional;
    }

    public static <T> CustomOptional<T> empty() {
        return new CustomOptional<>();
    }

    public static <T> CustomOptional<T> of(final T value) {
        return new CustomOptional<>(value);
    }

    public static <T> CustomOptional<T> ofNullable(final T value) {
        return (value == null) ? empty() : of(value);
    }

    public T get() {
        return optional.get();
    }

    public boolean isPresent() {
        return optional.isPresent();
    }

    public void ifPresent(final Consumer<? super T> consumer) {
        optional.ifPresent(consumer);
    }

    public CustomOptional<T> filter(final Predicate<? super T> predicate) {
        return new CustomOptional<>(optional.filter(predicate));
    }

    public <U> CustomOptional<U> map(final Function<? super T, ? extends U> mapper) {
        return new CustomOptional<>(optional.map(mapper));
    }

    public <U> CustomOptional<U> flatMap(final Function<? super T, ? extends CustomOptional<U>> mapper) {
        return new CustomOptional<>(optional.flatMap(mapper.andThen(cu -> cu.getOptional())));
    }

    public T orElse(final T other) {
        return optional.orElse(other);
    }

    public T orElseGet(final Supplier<? extends T> other) {
        return optional.orElseGet(other);
    }

    public <X extends Throwable> T orElseThrow(final Supplier<? extends X> exceptionSuppier) throws X {
        return optional.orElseThrow(exceptionSuppier);
    }

    public Stream<T> flatStream() {
        if (!optional.isPresent()) {
            return Stream.empty();
        }
        return Stream.of(get());
    }

    public T getTOrNull() {
        if (!optional.isPresent()) {
            return null;
        }
        return get();
    }

    @Override
    public boolean equals(final Object obj) {
        return optional.equals(obj);
    }

    @Override
    public int hashCode() {
        return optional.hashCode();
    }

    @Override
    public String toString() {
        return optional.toString();
    }
}

Sie werden sehen, dass ich flatStream()wie hier hinzugefügt habe:

public Stream<T> flatStream() {
    if (!optional.isPresent()) {
        return Stream.empty();
    }
    return Stream.of(get());
}

Benutzt als:

String result = Stream.of("a", "b", "c", "de", "fg", "hij")
        .map(this::resolve)
        .flatMap(CustomOptional::flatStream)
        .findFirst()
        .get();

Sie müssen hier immer noch ein zurückgeben Stream<T>, da Sie nicht zurückgeben können T, denn wenn !optional.isPresent(), dann, T == nullwenn Sie es als solches deklarieren, dann .flatMap(CustomOptional::flatStream)würden Sie versuchen, nulles einem Stream hinzuzufügen , und das ist nicht möglich.

Zum Beispiel:

public T getTOrNull() {
    if (!optional.isPresent()) {
        return null;
    }
    return get();
}

Benutzt als:

String result = Stream.of("a", "b", "c", "de", "fg", "hij")
        .map(this::resolve)
        .map(CustomOptional::getTOrNull)
        .findFirst()
        .get();

Werft jetzt einen NullPointerExceptionin den Stream Operationen.

Fazit

Die Methode, die Sie verwendet haben, ist tatsächlich die beste Methode.


6

Eine etwas kürzere Version mit reduce:

things.stream()
  .map(this::resolve)
  .reduce(Optional.empty(), (a, b) -> a.isPresent() ? a : b );

Sie können die Reduzierungsfunktion auch in eine statische Dienstprogrammmethode verschieben und dann wird es:

  .reduce(Optional.empty(), Util::firstPresent );

6
Ich mag das, aber es ist erwähnenswert, dass dies jedes Element im Stream auswertet, während findFirst () nur auswertet, bis es ein vorhandenes Element findet.
Duncan McGregor

1
Und leider ist die Ausführung jeder Lösung ein Deal-Breaker. Aber es ist klug.
Yona Appletree

5

Da meine vorherige Antwort nicht sehr beliebt zu sein schien, werde ich es noch einmal versuchen.

Eine kurze Antwort:

Sie sind meistens auf dem richtigen Weg. Der kürzeste Code, um zu Ihrer gewünschten Ausgabe zu gelangen, ist folgender:

things.stream()
      .map(this::resolve)
      .filter(Optional::isPresent)
      .findFirst()
      .flatMap( Function.identity() );

Dies wird all Ihren Anforderungen entsprechen:

  1. Es wird eine erste Antwort gefunden, die sich in eine Nicht-Leere auflöst Optional<Result>
  2. Es ruft this::resolvenach Bedarf faul an
  3. this::resolve wird nach dem ersten nicht leeren Ergebnis nicht aufgerufen
  4. Es wird zurückkehren Optional<Result>

Längere Antwort

Die einzige Änderung im Vergleich zur ursprünglichen OP-Version war, dass ich sie .map(Optional::get)vor dem Aufruf entfernt .findFirst()und .flatMap(o -> o)als letzten Aufruf in der Kette hinzugefügt habe .

Dies hat einen schönen Effekt, wenn das Double-Optional entfernt wird, wenn der Stream ein tatsächliches Ergebnis findet.

In Java kann man nicht kürzer sein.

Das alternative Codeausschnitt, das die konventionellere forSchleifentechnik verwendet, besteht aus ungefähr der gleichen Anzahl von Codezeilen und hat mehr oder weniger die gleiche Reihenfolge und Anzahl von Operationen, die Sie ausführen müssen:

  1. Anrufen this.resolve,
  2. Filterung basierend auf Optional.isPresent
  3. Rückgabe des Ergebnisses und
  4. eine Möglichkeit, mit negativen Ergebnissen umzugehen (wenn nichts gefunden wurde)

Um zu beweisen, dass meine Lösung wie angekündigt funktioniert, habe ich ein kleines Testprogramm geschrieben:

public class StackOverflow {

    public static void main( String... args ) {
        try {
            final int integer = Stream.of( args )
                    .peek( s -> System.out.println( "Looking at " + s ) )
                    .map( StackOverflow::resolve )
                    .filter( Optional::isPresent )
                    .findFirst()
                    .flatMap( o -> o )
                    .orElseThrow( NoSuchElementException::new )
                    .intValue();

            System.out.println( "First integer found is " + integer );
        }
        catch ( NoSuchElementException e ) {
            System.out.println( "No integers provided!" );
        }
    }

    private static Optional<Integer> resolve( String string ) {
        try {
            return Optional.of( Integer.valueOf( string ) );
        }
        catch ( NumberFormatException e )
        {
            System.out.println( '"' + string + '"' + " is not an integer");
            return Optional.empty();
        }
    }

}

(Es gibt nur wenige zusätzliche Zeilen zum Debuggen und Überprüfen, dass nur so viele Aufrufe wie nötig aufgelöst werden müssen ...)

Wenn ich dies über eine Befehlszeile ausführe, erhalte ich die folgenden Ergebnisse:

$ java StackOferflow a b 3 c 4
Looking at a
"a" is not an integer
Looking at b
"b" is not an integer
Looking at 3
First integer found is 3

Ich denke genauso wie Roland Tepp. Warum sollte jemand Stream <Stream <? >> und flach machen, wenn Sie nur mit einem optionalen <optional <? >>
Young Hyun Yoo

3

Wenn es Ihnen nichts ausmacht, eine Bibliothek eines Drittanbieters zu verwenden, können Sie Javaslang verwenden . Es ist wie Scala, aber in Java implementiert.

Es kommt mit einer vollständigen unveränderlichen Sammlungsbibliothek, die der von Scala bekannten sehr ähnlich ist. Diese Sammlungen ersetzen die Sammlungen von Java und den Stream von Java 8. Es hat auch eine eigene Implementierung von Option.

import javaslang.collection.Stream;
import javaslang.control.Option;

Stream<Option<String>> options = Stream.of(Option.some("foo"), Option.none(), Option.some("bar"));

// = Stream("foo", "bar")
Stream<String> strings = options.flatMap(o -> o);

Hier ist eine Lösung für das Beispiel der Ausgangsfrage:

import javaslang.collection.Stream;
import javaslang.control.Option;

public class Test {

    void run() {

        // = Stream(Thing(1), Thing(2), Thing(3))
        Stream<Thing> things = Stream.of(new Thing(1), new Thing(2), new Thing(3));

        // = Some(Other(2))
        Option<Other> others = things.flatMap(this::resolve).headOption();
    }

    Option<Other> resolve(Thing thing) {
        Other other = (thing.i % 2 == 0) ? new Other(i + "") : null;
        return Option.of(other);
    }

}

class Thing {
    final int i;
    Thing(int i) { this.i = i; }
    public String toString() { return "Thing(" + i + ")"; }
}

class Other {
    final String s;
    Other(String s) { this.s = s; }
    public String toString() { return "Other(" + s + ")"; }
}

Haftungsausschluss: Ich bin der Schöpfer von Javaslang.


3

Spät zur Party, aber was ist mit

things.stream()
    .map(this::resolve)
    .filter(Optional::isPresent)
    .findFirst().get();

Sie können das letzte get () entfernen, wenn Sie eine util-Methode zum manuellen Konvertieren von optional in stream erstellen:

things.stream()
    .map(this::resolve)
    .flatMap(Util::optionalToStream)
    .findFirst();

Wenn Sie den Stream sofort von Ihrer Auflösungsfunktion zurückgeben, speichern Sie eine weitere Zeile.


3

Ich möchte Factory-Methoden zum Erstellen von Hilfsprogrammen für funktionale APIs fördern :

Optional<R> result = things.stream()
        .flatMap(streamopt(this::resolve))
        .findFirst();

Die Fabrikmethode:

<T, R> Function<T, Stream<R>> streamopt(Function<T, Optional<R>> f) {
    return f.andThen(Optional::stream); // or the J8 alternative:
    // return t -> f.apply(t).map(Stream::of).orElseGet(Stream::empty);
}

Argumentation:

  • Wie bei Methodenreferenzen im Allgemeinen können Sie im Vergleich zu Lambda-Ausdrücken nicht versehentlich eine Variable aus dem zugänglichen Bereich erfassen, z.

    t -> streamopt(resolve(o))

  • Es ist zusammensetzbar, Sie können zB Function::andThendas Ergebnis der Factory-Methode aufrufen :

    streamopt(this::resolve).andThen(...)

    Während im Fall eines Lambda Sie es zuerst gießen müssen:

    ((Function<T, Stream<R>>) t -> streamopt(resolve(t))).andThen(...)



3

Wenn Sie mit Java 8 nicht weiterkommen, aber Zugriff auf Guava 21.0 oder höher haben, können Sie verwenden Streams.stream eine Option in einen Stream konvertieren.

Also gegeben

import com.google.common.collect.Streams;

Du kannst schreiben

Optional<Other> result =
    things.stream()
        .map(this::resolve)
        .flatMap(Streams::stream)
        .findFirst();

0

Was ist damit?

private static List<String> extractString(List<Optional<String>> list) {
    List<String> result = new ArrayList<>();
    list.forEach(element -> element.ifPresent(result::add));
    return result;
}

https://stackoverflow.com/a/58281000/3477539


Warum tun Sie dies, wenn Sie streamen und sammeln können?
OneCricketeer

return list.stream().filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList())), genau wie die Frage (und Ihre verknüpfte Antwort) ...
OneCricketeer

Ich kann mich irren, aber ich denke, dass die Verwendung von isPresent () und get () keine gute Praxis ist. Also versuche ich davon wegzukommen.
Rastaman

Wenn Sie .get() ohne verwenden isPresent() , erhalten Sie eine Warnung in IntelliJ
OneCricketeer

-5

Höchstwahrscheinlich machst du es falsch.

Java 8 Optional ist nicht für diese Verwendung vorgesehen. Es ist normalerweise nur für Terminal-Stream-Operationen reserviert, die einen Wert zurückgeben können oder nicht, wie zum Beispiel find.

In Ihrem Fall ist es möglicherweise besser, zuerst zu versuchen, einen kostengünstigen Weg zu finden, um die auflösbaren Elemente herauszufiltern, und dann das erste Element als optional zu erhalten und es als letzten Vorgang aufzulösen. Besser noch - anstatt zu filtern, suchen Sie das erste auflösbare Element und lösen Sie es auf.

things.filter(Thing::isResolvable)
      .findFirst()
      .flatMap(this::resolve)
      .get();

Als Faustregel gilt, dass Sie sich bemühen sollten, die Anzahl der Elemente im Stream zu reduzieren, bevor Sie sie in etwas anderes umwandeln. YMMV natürlich.


6
Ich denke, dass die Methode resolve () des OP, die Optional <Other> zurückgibt, eine durchaus sinnvolle Verwendung von Optional ist. Ich kann natürlich nicht mit der Problemdomäne des OP sprechen, aber es könnte sein, dass der Weg, um festzustellen, ob etwas auflösbar ist, darin besteht, zu versuchen, es zu lösen. Wenn ja, verschmilzt Optional ein boolesches Ergebnis von "war dies auflösbar" mit dem Ergebnis der Auflösung, falls erfolgreich, in einem einzigen API-Aufruf.
Stuart Marks

2
Stuart ist grundsätzlich richtig. Ich habe eine Reihe von Suchbegriffen in der Reihenfolge ihrer Erwünschtheit und suche nach dem Ergebnis des ersten, der etwas zurückgibt. Also im Grunde Optional<Result> searchFor(Term t). Das scheint der Absicht von Optional zu entsprechen. Außerdem sollten stream () s träge ausgewertet werden, sodass keine zusätzliche Arbeit zum Auflösen von Begriffen nach dem ersten übereinstimmenden Begriff erfolgen sollte.
Yona Appletree

Die Frage ist durchaus sinnvoll und die Verwendung von flatMap mit Optional wird häufig in anderen, ähnlichen Programmiersprachen wie Scala praktiziert.
dzs
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.