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.