Utilizzo di Java 8 opzionale con Stream :: flatMap


240

Il nuovo framework e gli amici del flusso Java 8 creano un codice java molto conciso, ma mi sono imbattuto in una situazione apparentemente semplice che è difficile da fare in modo conciso.

Considera un List<Thing> thingse metodo Optional<Other> resolve(Thing thing). Voglio mappare le Things su se Optional<Other>ottenere il primo Other. La soluzione ovvia sarebbe quella di utilizzare things.stream().flatMap(this::resolve).findFirst(), ma flatMaprichiede che tu restituisca uno stream e Optionalnon abbia un stream()metodo (o è un Collectiono fornisce un metodo per convertirlo o visualizzarlo come a Collection).

Il meglio che posso inventare è questo:

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

Ma questo sembra terribilmente complicato per quello che sembra un caso molto comune. Qualcuno ha un'idea migliore?


Dopo aver codificato un po 'con il tuo esempio, in realtà trovo la versione esplicita più leggibile di quella relativa, se fosse esistita .flatMap(Optional::toStream), con la tua versione in realtà vedi cosa sta succedendo.
Skiwi,

19
@skiwi Bene, Optional.streamesiste in JDK 9 ora ....
Stuart segna il

Sono curioso di sapere dove sia documentato e quale sia stato il processo per ottenerlo. Esistono altri metodi che sembrano realmente esistere e sono curioso di sapere dove si stia svolgendo la discussione per le modifiche alle API.
Yona Appletree


10
La cosa divertente è che JDK-8050820 in realtà fa riferimento a questa domanda nella sua descrizione!
Didier L

Risposte:


265

Java 9

Optional.stream è stato aggiunto a JDK 9. Ciò consente di eseguire le seguenti operazioni, senza la necessità di alcun metodo di supporto:

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

Java 8

Sì, questo era un piccolo buco nell'API, in quanto è in qualche modo scomodo trasformare un Optional<T>in una lunghezza zero o uno Stream<T>. Potresti farlo:

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

Avere l'operatore ternario dentro flatMapè un po 'complicato, quindi potrebbe essere meglio scrivere una piccola funzione di aiuto per fare questo:

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

Qui, ho sottolineato la chiamata resolve()invece di avere un'operazione separata map(), ma questa è una questione di gusti.


2
Non credo che l'API possa cambiare fino a Java 9 ora.
Assylias,

5
@Hypher Grazie. La tecnica .filter (). Map () non è male ed evita le dipendenze dai metodi di supporto. 'Sarebbe bello se ci fosse un modo più conciso però. Esaminerò come aggiungere Optional.stream ().
Stuart segna il

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

5
Vorrei che aggiungessero solo un Optionalsovraccarico a Stream#flatMap... in quel modo si potrebbe semplicemente scriverestream().flatMap(this::resolve)
fiocchi

4
@flkes Sì, abbiamo dato il via a questa idea, ma non sembra aggiungere tutto quel valore ora che (in JDK 9) c'è Optional.stream().
Stuart segna il

69

Sto aggiungendo questa seconda risposta basata su una modifica proposta dall'utente srborlongan all'altra mia risposta . Penso che la tecnica proposta fosse interessante, ma non era davvero adatta come modifica alla mia risposta. Altri hanno concordato e la modifica proposta è stata votata verso il basso. (Non ero uno degli elettori.) La tecnica ha comunque dei meriti. Sarebbe stato meglio se srborlongan avesse pubblicato la propria risposta. Questo non è ancora successo e non volevo che la tecnica andasse persa nelle nebbie della cronologia delle modifiche rifiutata da StackOverflow, quindi ho deciso di presentarla come risposta separata.

Fondamentalmente la tecnica consiste nell'utilizzare alcuni dei Optionalmetodi in modo intelligente per evitare di dover usare un operatore ternario ( ? :) o un'istruzione if / else.

Il mio esempio inline verrebbe riscritto in questo modo:

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

Un mio esempio che utilizza un metodo helper verrebbe riscritto in questo modo:

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

COMMENTO

Confrontiamo direttamente le versioni originali vs modificate:

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

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

L'originale è un approccio semplice se simile all'operaio: otteniamo un Optional<Other>; se ha un valore, restituiamo un flusso contenente quel valore e se non ha alcun valore, restituiamo un flusso vuoto. Abbastanza semplice e facile da spiegare.

La modifica è intelligente e ha il vantaggio di evitare i condizionali. (So ​​che ad alcune persone non piace l'operatore ternario. Se usato in modo improprio può davvero rendere il codice difficile da capire.) Tuttavia, a volte le cose possono essere troppo intelligenti. Il codice modificato inizia anche con un Optional<Other>. Quindi chiama Optional.mapche è definito come segue:

Se è presente un valore, applicare ad esso la funzione di mapping fornita e, se il risultato è diverso da null, restituire un Opzionale che descrive il risultato. Altrimenti restituire un Opzionale vuoto.

La map(Stream::of)chiamata restituisce un Optional<Stream<Other>>. Se un valore era presente nell'input facoltativo, l'Opzionale restituito contiene un flusso che contiene il singolo altro risultato. Ma se il valore non era presente, il risultato è un Opzionale vuoto.

Successivamente, la chiamata a orElseGet(Stream::empty)restituisce un valore di tipo Stream<Other>. Se il suo valore di input è presente, ottiene il valore, che è l'elemento singolo Stream<Other>. Altrimenti (se il valore di input è assente) restituisce un valore vuoto Stream<Other>. Quindi il risultato è corretto, lo stesso del codice condizionale originale.

Nei commenti sulla mia risposta, riguardo alla modifica rifiutata, avevo descritto questa tecnica come "più concisa ma anche più oscura". Sono qui. Mi ci è voluto un po 'di tempo per capire cosa stesse facendo e mi ci è voluto anche un po' per scrivere la descrizione sopra di quello che stava facendo. La sottigliezza chiave è la trasformazione da Optional<Other>a Optional<Stream<Other>>. Una volta che hai capito, ha senso, ma non era ovvio per me.

Riconoscerò, tuttavia, che le cose inizialmente oscure possono diventare idiomatiche nel tempo. Potrebbe essere che questa tecnica finisca per essere il modo migliore in pratica, almeno fino a quando non Optional.streamviene aggiunta (se mai lo fa).

AGGIORNAMENTO: Optional.stream è stato aggiunto a JDK 9.


16

Non puoi farlo in modo più conciso come già stai facendo.

Sostieni di non volerlo .filter(Optional::isPresent) e .map(Optional::get) .

Ciò è stato risolto dal metodo descritto da @StuartMarks, tuttavia di conseguenza ora lo si mappa su un Optional<T>, quindi ora è necessario utilizzare .flatMap(this::streamopt)e un get()alla fine.

Quindi è ancora composto da due affermazioni e ora puoi ottenere eccezioni con il nuovo metodo! Perché, se ogni opzione fosse vuota? Quindi findFirst()restituirà un optional vuoto e il tuo get()fallirà!

Quindi quello che hai:

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

è in realtà il modo migliore per ottenere ciò che vuoi, e cioè vuoi salvare il risultato come un T, non come un Optional<T>.

Mi sono permesso di creare una CustomOptional<T>classe che avvolge l' Optional<T>e fornisce un metodo in più, flatStream(). Si noti che non è possibile estendere 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();
    }
}

Vedrai che ho aggiunto flatStream(), come qui:

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

Usato come:

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

Dovrai comunque restituire un Stream<T>qui, poiché non puoi tornare T, perché se !optional.isPresent(), quindi T == nullse lo dichiari tale, ma allora .flatMap(CustomOptional::flatStream)tenteresti di aggiungere nulluno stream e ciò non è possibile.

Per esempio:

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

Usato come:

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

Ora lancerà NullPointerExceptionall'interno delle operazioni del flusso.

Conclusione

Il metodo che hai usato è in realtà il metodo migliore.


6

Una versione leggermente più corta usando reduce:

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

È inoltre possibile spostare la funzione di riduzione su un metodo di utilità statica e quindi diventa:

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

6
Mi piace questo, ma vale la pena sottolineare che questo valuterà ogni elemento nello Stream, mentre findFirst () valuterà solo fino a quando non trova un elemento presente.
Duncan McGregor,

1
E sfortunatamente, l'esecuzione di ogni soluzione è un fallimento. Ma è intelligente.
Yona Appletree,

5

Dato che la mia risposta precedente sembrava non essere molto popolare, ci darò un'altra possibilità.

Una breve risposta:

Sei per lo più sulla buona strada. Il codice più breve per ottenere l'output desiderato che ho potuto trovare è questo:

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

Questo si adatta a tutte le vostre esigenze:

  1. Troverà la prima risposta che si risolve in non vuota Optional<Result>
  2. Chiama this::resolvepigramente, se necessario
  3. this::resolve non verrà chiamato dopo il primo risultato non vuoto
  4. Tornerà Optional<Result>

Risposta più lunga

L'unica modifica rispetto alla versione iniziale dell'OP è stata che ho rimosso .map(Optional::get)prima della chiamata .findFirst()e aggiunto .flatMap(o -> o)come ultima chiamata nella catena.

Questo ha un buon effetto nel liberarsi del doppio-Opzionale, ogni volta che lo stream trova un risultato reale.

Non puoi davvero andare più corto di così in Java.

Lo snippet alternativo di codice che utilizza la fortecnica di loop più convenzionale sarà circa lo stesso numero di righe di codice e avrà più o meno lo stesso ordine e numero di operazioni necessarie per eseguire:

  1. Chiamando this.resolve,
  2. filtro basato su Optional.isPresent
  3. restituendo il risultato e
  4. un modo di affrontare il risultato negativo (quando non è stato trovato nulla)

Giusto per dimostrare che la mia soluzione funziona come pubblicizzato, ho scritto un piccolo programma di test:

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

}

(Ha poche righe extra per il debug e la verifica che solo quante chiamate si risolvano secondo necessità ...)

Eseguendo questo da una riga di comando, ho ottenuto i seguenti risultati:

$ 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

Penso lo stesso di Roland Tepp. Perché qualcuno dovrebbe rendere lo stream <stream <? >> e piatto quando puoi semplicemente flat con uno <opzionale <? >>
Young Hyun Yoo,

3

Se non ti dispiace utilizzare una libreria di terze parti, puoi utilizzare Javaslang . È come Scala, ma implementato in Java.

Viene fornito con una libreria di raccolta immutabile completa che è molto simile a quella conosciuta da Scala. Queste raccolte sostituiscono le raccolte Java e lo Stream di Java 8. Ha anche una propria implementazione dell'Opzione.

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

Ecco una soluzione per l'esempio della domanda iniziale:

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

Disclaimer: sono il creatore di Javaslang.


3

In ritardo alla festa, ma che dire

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

Puoi liberarti dell'ultimo get () se crei un metodo util per convertire manualmente lo streaming in opzionale:

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

Se si restituisce immediatamente lo stream dalla funzione di risoluzione, si salva un'altra riga.


3

Vorrei promuovere metodi di fabbrica per la creazione di helper per API funzionali:

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

Il metodo di fabbrica:

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

Ragionamento:

  • Come per i riferimenti ai metodi in generale, rispetto alle espressioni lambda, non è possibile acquisire accidentalmente una variabile dall'ambito accessibile, come:

    t -> streamopt(resolve(o))

  • È compostabile, ad esempio è possibile chiamare Function::andThenil risultato del metodo di fabbrica:

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

    Considerando che nel caso di un lambda, dovresti prima lanciarlo:

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


3

Null è supportato dallo Stream fornito La mia libreria AbacusUtil . Ecco il codice:

Stream.of(things).map(e -> resolve(e).orNull()).skipNull().first();

3

Se sei bloccato con Java 8 ma hai accesso a Guava 21.0 o versioni successive, puoi utilizzare Streams.streamper convertire un facoltativo in uno stream.

Quindi, dato

import com.google.common.collect.Streams;

tu puoi scrivere

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

0

Che ne dici?

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


Perché farlo quando è possibile eseguire lo streaming e la raccolta?
OneCricketeer il

return list.stream().filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList())), proprio come la domanda (e la tua risposta collegata) ha ...
OneCricketeer,

Potrei sbagliarmi, ma considero l'utilizzo di isPresent () e quindi get () non è una buona pratica. Quindi provo ad allontanarmi da quello.
rastaman,

Se lo usi .get() senza isPresent() , allora ricevi un avviso in IntelliJ
OneCricketeer il

-5

Molto probabilmente lo stai facendo male.

Java 8 Opzionale non è pensato per essere utilizzato in questo modo. Di solito è riservato solo alle operazioni del flusso terminale che possono o non possono restituire un valore, come ad esempio find.

Nel tuo caso potrebbe essere meglio provare prima a trovare un modo economico per filtrare quegli elementi che sono risolvibili, quindi ottenere il primo elemento come opzionale e risolverlo come ultima operazione. Meglio ancora: invece di filtrare, trova il primo elemento risolvibile e risolvilo.

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

La regola empirica è che dovresti cercare di ridurre il numero di elementi nello stream prima di trasformarli in qualcos'altro. YMMV ovviamente.


6
Penso che il metodo resol () dell'OP che ritorna Opzionale <Altro> sia un uso perfettamente sensato di Opzionale. Ovviamente non posso parlare con il dominio problematico del PO, ma potrebbe essere che il modo per determinare se qualcosa è risolvibile sia tentare di risolverlo. In tal caso, Opzionale fonde un risultato booleano di "era risolvibile" con il risultato della risoluzione, in caso di successo, in una singola chiamata API.
Stuart segna il

2
Stuart è sostanzialmente corretto. Ho una serie di termini di ricerca in ordine di desiderabilità e sto cercando di trovare il risultato del primo che restituisce qualcosa. Quindi sostanzialmente Optional<Result> searchFor(Term t). Ciò sembra corrispondere all'intenzione di Opzionale. Inoltre, stream () dovrebbe essere valutato pigramente, quindi non dovrebbero verificarsi termini di risoluzione del lavoro extra oltre il primo corrispondente.
Yona Appletree

La domanda è perfettamente sensata e l'utilizzo di flatMap con Optional è spesso praticato in altri linguaggi di programmazione simili, come Scala.
dz
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.