Opcjonalne używanie Java 8 z Stream :: flatMap


240

Nowa struktura strumieniowa Java 8 i znajomi tworzą bardzo zwięzły kod java, ale natknąłem się na pozornie prostą sytuację, której wykonanie jest trudne.

Zastanów się nad List<Thing> thingsmetodą i Optional<Other> resolve(Thing thing). Chcę zmapować Things do Optional<Other>s i uzyskać pierwszy Other. Oczywistym rozwiązaniem byłoby użycie things.stream().flatMap(this::resolve).findFirst(), ale flatMapwymaga zwrotu strumienia i Optionalnie ma stream()metody (lub jest to Collectionmetoda lub udostępnia metodę konwersji na nią lub wyświetlenia jako a Collection).

Najlepsze, co mogę wymyślić, to:

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

Ale to wydaje się okropnie długo rozwiązywane, co wydaje się bardzo częstym przypadkiem. Czy ktoś ma lepszy pomysł?


Po trochę zakodowaniu w twoim przykładzie, uważam, że wersja jawna jest bardziej czytelna niż ta, która dotyczy, jeśli istniała .flatMap(Optional::toStream), w twojej wersji faktycznie widzisz, co się dzieje.
skiwi

19
@skiwi Cóż, Optional.streamistnieje już w JDK 9 ....
Stuart Marks

Jestem ciekawy, gdzie to jest udokumentowane i jaki był proces uzyskania tego. Istnieje kilka innych metod, które naprawdę wydają się istnieć, i jestem ciekawy, gdzie odbywa się dyskusja na temat zmian API.
Yona Appletree


10
Zabawne jest to, że JDK-8050820 tak naprawdę odnosi się do tego pytania w swoim opisie!
Didier L,

Odpowiedzi:


265

Java 9

Optional.stream został dodany do JDK 9. Umożliwia to wykonywanie następujących czynności, bez potrzeby stosowania żadnej metody pomocniczej:

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

Java 8

Tak, to była niewielka dziura w interfejsie API, ponieważ nieco niewygodne jest przekształcenie Optional<T>w długość zero lub jeden Stream<T>. Możesz to zrobić:

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

Posiadanie operatora trójskładnikowego w środku flatMapjest jednak nieco kłopotliwe, więc może być lepiej napisać małą funkcję pomocniczą, aby to zrobić:

/**
 * 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();

Tutaj podałem wezwanie do resolve()zamiast osobnej map()operacji, ale to kwestia gustu.


2
Nie sądzę, aby interfejs mógł się zmienić aż do wersji Java 9.
assylias

5
@Hypher Thanks. Technika .filter (). Map () nie jest taka zła i pozwala uniknąć zależności od metod pomocniczych. „Byłoby fajnie, gdyby istniał bardziej zwięzły sposób. Zbadam, czy dodano Optional.stream ().
Stuart Marks

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

5
Żałuję, że nie dodaliby Optionalprzeciążenia do Stream#flatMap... w ten sposób można po prostu pisaćstream().flatMap(this::resolve)
płatki

4
@flkes Tak, skopaliśmy ten pomysł, ale wydaje się, że nie dodaje on aż tak dużej wartości, skoro (w JDK 9) istnieje Optional.stream().
Stuart Marks

69

Dodaję drugą odpowiedź na podstawie proponowanej edycji przez użytkownika srborlongan do mojej drugiej odpowiedzi . Myślę, że zaproponowana technika była interesująca, ale nie nadawała się do edycji mojej odpowiedzi. Inni zgodzili się, a proponowana zmiana została odrzucona. (Nie byłem jednym z głosujących.) Ta technika ma jednak swoje zalety. Byłoby najlepiej, gdyby srborlongan opublikował własną odpowiedź. To się jeszcze nie zdarzyło i nie chciałem, aby technika zaginęła we mgle StackOverflow, odrzuciła historię edycji, więc postanowiłem przedstawić ją jako osobną odpowiedź.

Zasadniczo technika polega na użyciu niektórych Optionalmetod w sprytny sposób, aby uniknąć konieczności używania operatora trójskładnikowego ( ? :) lub instrukcji if / else.

Mój wbudowany przykład zostałby przepisany w ten sposób:

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

Mój przykład wykorzystujący metodę pomocniczą zostałby przepisany w następujący sposób:

/**
 * 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();

KOMENTARZ

Porównajmy bezpośrednio oryginalne i zmodyfikowane wersje:

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

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

Oryginał jest prosty, jeśli roboczy podejście: otrzymujemy Optional<Other>; jeśli ma wartość, zwracamy strumień zawierający tę wartość, a jeśli nie ma żadnej wartości, zwracamy pusty strumień. Całkiem proste i łatwe do wyjaśnienia.

Modyfikacja jest sprytna i ma tę zaletę, że pozwala uniknąć warunków warunkowych. (Wiem, że niektórzy ludzie nie lubią trójskładnikowego operatora. W przypadku niewłaściwego użycia, kod może być trudny do zrozumienia). Czasami jednak rzeczy mogą być zbyt sprytne. Zmodyfikowany kod zaczyna się również od Optional<Other>. Następnie wywołuje, Optional.mapco jest zdefiniowane następująco:

Jeśli wartość jest obecna, zastosuj do niej podaną funkcję odwzorowania, a jeśli wynik nie jest pusty, zwróć Opcjonalne opisujące wynik. W przeciwnym razie zwróć puste Opcjonalne.

map(Stream::of)Wywołanie zwraca Optional<Stream<Other>>. Jeśli wartość wejściowa była obecna Opcjonalnie, zwrócona Opcjonalnie zawiera strumień, który zawiera pojedynczy wynik Inne. Ale jeśli wartość nie była obecna, wynikiem jest pusta opcja.

Następnie wywołanie orElseGet(Stream::empty)zwraca wartość typu Stream<Other>. Jeśli jego wartość wejściowa jest obecna, otrzymuje wartość, która jest pojedynczym elementem Stream<Other>. W przeciwnym razie (jeśli wartość wejściowa jest nieobecna) zwraca wartość pustą Stream<Other>. Tak więc wynik jest poprawny, taki sam jak oryginalny kod warunkowy.

W komentarzach do mojej odpowiedzi dotyczącej odrzuconej edycji opisałem tę technikę jako „bardziej zwięzłą, ale także bardziej niejasną”. Stoję przy tym. Zajęło mi trochę czasu, aby dowiedzieć się, co robi, a także zajęło mi trochę czasu, aby napisać powyższy opis tego, co robi. Kluczową subtelnością jest transformacja z Optional<Other>na Optional<Stream<Other>>. Kiedy już to zrozumiesz, ma to sens, ale nie było dla mnie oczywiste.

Przyznaję jednak, że rzeczy, które początkowo są niejasne, z czasem mogą stać się idiomatyczne. Może się zdarzyć, że ta technika stanie się najlepszym sposobem w praktyce, przynajmniej do czasu Optional.streamdodania (jeśli w ogóle).

AKTUALIZACJA: Optional.stream dodano do JDK 9.


16

Nie możesz zrobić tego bardziej zwięźle, jak już to robisz.

Twierdzisz, że nie chcesz .filter(Optional::isPresent) i .map(Optional::get) .

Zostało to rozwiązane za pomocą metody @StuartMarks, jednak w rezultacie mapujesz go teraz na Optional<T>, więc teraz musisz go użyć .flatMap(this::streamopt)i get()na końcu.

Więc nadal składa się z dwóch instrukcji i teraz możesz uzyskać wyjątki dzięki nowej metodzie! Bo co jeśli każda opcja jest pusta? Wtedy findFirst()zwróci pustą opcjonalną i twoja get()porażka!

Co masz:

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

jest w rzeczywistości najlepszym sposobem na osiągnięcie tego, czego chcesz, i chcesz zapisać wynik jako, a Tnie jako Optional<T>.

Pozwoliłem sobie tworzenia CustomOptional<T>klasy, która otula Optional<T>i zapewnia dodatkową metodę flatStream(). Pamiętaj, że nie możesz przedłużyć 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();
    }
}

Zobaczysz, że dodałem flatStream(), jak tutaj:

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

Użyty jako:

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

Państwo nadal będą musiały zwracają Stream<T>tutaj, ponieważ nie może wrócić T, bo jeśli !optional.isPresent(), to T == nulljeśli uznaniu jej za takie, ale wówczas .flatMap(CustomOptional::flatStream)będzie próbował dodać nulldo strumienia i że nie jest to możliwe.

Jako przykład:

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

Użyty jako:

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

Teraz rzuci NullPointerExceptionoperację wewnątrz strumienia.

Wniosek

Zastosowana metoda jest w rzeczywistości najlepszą metodą.


6

Nieco krótsza wersja wykorzystująca reduce:

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

Możesz również przenieść funkcję zmniejszania do statycznej metody użyteczności, a wtedy stanie się ona:

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

6
Podoba mi się to, ale warto zaznaczyć, że to oceni każdy element w strumieniu, podczas gdy findFirst () będzie oceniać tylko do momentu znalezienia obecnego elementu.
Duncan McGregor,

1
I niestety wykonanie każdego rozstrzygnięcia jest przełomowe. Ale to sprytne.
Yona Appletree

5

Ponieważ moja poprzednia odpowiedź nie wydawała się zbyt popularna, spróbuję jeszcze raz.

Krótka odpowiedź:

Przeważnie jesteś na dobrej drodze. Najkrótszy kod, aby uzyskać pożądany wynik, jaki mogę wymyślić, to:

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

To spełni wszystkie Twoje wymagania:

  1. Znajdzie pierwszą odpowiedź, która rozwiązuje się jako niepusta Optional<Result>
  2. Domaga this::resolvesię leniwie, ile potrzeba
  3. this::resolve nie zostanie wywołany po pierwszym niepustym wyniku
  4. Wróci Optional<Result>

Dłuższa odpowiedź

Jedyną modyfikacją w porównaniu do początkowej wersji OP było to, że usunąłem .map(Optional::get)przed połączeniem .findFirst()i dodałem .flatMap(o -> o)jako ostatnie połączenie w łańcuchu.

Daje to przyjemny efekt pozbycia się podwójnej Opcji, ilekroć strumień znajdzie rzeczywisty wynik.

Tak naprawdę nie można iść krócej niż to w Javie.

Alternatywny fragment kodu wykorzystujący bardziej konwencjonalną fortechnikę pętli będzie miał mniej więcej tę samą liczbę wierszy kodu i będzie miał mniej więcej taką samą kolejność i liczbę operacji, które należy wykonać:

  1. wywołanie this.resolve,
  2. filtrowanie na podstawie Optional.isPresent
  3. zwracanie wyniku i
  4. jakiś sposób radzenia sobie z wynikiem negatywnym (kiedy nic nie znaleziono)

Aby udowodnić, że moje rozwiązanie działa zgodnie z reklamą, napisałem mały program testowy:

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();
        }
    }

}

(Ma kilka dodatkowych linii do debugowania i weryfikacji, że tylko tyle wywołań do rozwiązania, ile potrzeba ...)

Wykonując to w wierszu polecenia, otrzymałem następujące wyniki:

$ 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

Myślę tak samo jak Roland Tepp. Dlaczego ktoś miałby robić strumień <strumień <? >> i płaski, skoro można po prostu płasko za pomocą jednego opcjonalnego <opcjonalnego <? >>
Young Hyun Yoo

3

Jeśli nie masz nic przeciwko korzystaniu z biblioteki innej firmy, możesz użyć JavaScript . Jest jak Scala, ale zaimplementowany w Javie.

Jest wyposażony w kompletną niezmienną bibliotekę kolekcji, która jest bardzo podobna do znanej ze Scali. Te kolekcje zastępują kolekcje Javy i Java 8's Stream. Ma także własną implementację Opcji.

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);

Oto rozwiązanie dla przykładu pytania początkowego:

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 + ")"; }
}

Oświadczenie: Jestem twórcą języka JavaScript.


3

Późno na imprezę, ale co z tym

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

Możesz pozbyć się ostatniej metody get (), jeśli utworzysz metodę util do konwersji opcjonalnej do strumieniowego przesyłania strumieniowego:

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

Jeśli zwrócisz strumień od razu z funkcji rozstrzygania, zapisujesz jeszcze jedną linię.


3

Chciałbym promować fabryczne metody tworzenia pomocników dla funkcjonalnych interfejsów API:

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

Metoda fabryczna:

<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);
}

Rozumowanie:

  • Podobnie jak w przypadku odniesień do metod ogólnie, w porównaniu do wyrażeń lambda, nie można przypadkowo przechwycić zmiennej z dostępnego zakresu, na przykład:

    t -> streamopt(resolve(o))

  • Składa się, możesz np. Wywołać Function::andThenwynik metody fabrycznej:

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

    Podczas gdy w przypadku lambda musisz ją najpierw rzucić:

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



3

Jeśli utknąłeś w Javie 8, ale masz dostęp do Guava 21.0 lub nowszej wersji, możesz użyć Streams.streamdo konwersji opcjonalnej w strumień.

Tak więc dane

import com.google.common.collect.Streams;

Możesz pisać

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

0

Co z tym?

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


Po co to robić, skoro możesz przesyłać strumieniowo i zbierać?
OneCricketeer

return list.stream().filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList())), podobnie jak pytanie (i linkowana odpowiedź) ma ...
OneCricketeer

Mogę się mylić, ale uważam, że użycie isPresent (), a następnie get () nie jest dobrą praktyką. Więc staram się od tego uciec.
rastaman

Jeśli użyjesz go .get() bez isPresent() , otrzymasz ostrzeżenie w IntelliJ
OneCricketeer

-5

Najprawdopodobniej robisz to źle.

Java 8 Opcjonalna nie jest przeznaczona do użycia w ten sposób. Zazwyczaj jest zarezerwowane tylko dla operacji strumienia terminali, które mogą, ale nie muszą zwracać wartości, jak na przykład find.

W twoim przypadku może być lepiej najpierw spróbować znaleźć tani sposób, aby odfiltrować te elementy, które można rozwiązać, a następnie uzyskać pierwszy element jako opcjonalny i rozwiązać go jako ostatnią operację. Jeszcze lepiej - zamiast filtrować, znajdź pierwszy przedmiot do rozwiązania i rozwiąż go.

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

Ogólna zasada jest taka, że ​​powinieneś starać się zmniejszyć liczbę elementów w strumieniu, zanim przekształcisz je w coś innego. Oczywiście YMMV.


6
Myślę, że metoda resol () OP zwracająca Opcjonalne <Other> jest całkowicie rozsądnym zastosowaniem Opcjonalnego. Oczywiście nie mogę rozmawiać z domeną problemu PO, ale może być tak, że sposobem ustalenia, czy coś można rozwiązać, jest próba rozwiązania tego. Jeśli tak, Opcjonalne łączy wynik logiczny „było to możliwe do rozwiązania” z wynikiem rozstrzygnięcia, jeśli się powiedzie, w jednym wywołaniu API.
Stuart Marks

2
Stuart ma w zasadzie rację. Mam zestaw wyszukiwanych haseł w kolejności pożądanej i szukam wyniku pierwszego, który cokolwiek zwraca. Więc w zasadzie Optional<Result> searchFor(Term t). To wydaje się pasować do intencji Opcjonalnej. Ponadto stream () powinien być leniwie oceniany, więc nie powinno się pojawiać żadnych dodatkowych zadań rozwiązujących warunki po pierwszym pasującym.
Yona Appletree

Pytanie jest całkowicie rozsądne i często używa się flatMap z Opcjonalnym w innych podobnych językach programowania, takich jak Scala.
dzs
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.