Utilizzo di Java 8 facoltativo con Stream :: flatMap

Il nuovo framework di stream Java 8 e gli amici creano un codice java molto conciso, ma mi sono imbattuto in una situazione apparentemente semplice che è complicata da fare concisamente.

Considera una List things e metodo Optional resolve(Thing thing) . Voglio mappare la Thing a Optional e ottenere il primo Other . La soluzione più ovvia sarebbe usare things.stream().flatMap(this::resolve).findFirst() , ma flatMap richiede che tu restituisca un stream, e Optional non ha un metodo stream() (o è un Collection o fornire un metodo per convertirlo o visualizzarlo come una Collection ).

Il meglio che posso inventare è questo:

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

Ma sembra terribilmente prolisso per quello che sembra un caso molto comune. Qualcuno ha un’idea migliore?

Java 9

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

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

Java 8

Sì, questo era un piccolo buco nell’API, in quanto è un po ‘scomodo trasformare un Facoltativo in uno Stream di lunghezza zero o uno. Potresti fare questo:

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

Avere l’operatore ternario all’interno di flatMap è un po ‘macchinoso, quindi, potrebbe essere meglio scrivere una piccola funzione di aiuto per fare questo:

 /** * Turns an Optional into a Stream of length zero or one depending upon * whether a value is present. */ static  Stream streamopt(Optional opt) { if (opt.isPresent()) return Stream.of(opt.get()); else return Stream.empty(); } Optional result = things.stream() .flatMap(t -> streamopt(resolve(t))) .findFirst(); 

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

Sto aggiungendo questa seconda risposta basata su una modifica proposta dall’utente srborlongan alla mia altra risposta . Penso che la tecnica proposta fosse interessante, ma non è stata davvero adatta come una modifica alla mia risposta. Altri hanno accettato e la modifica proposta è stata votata in ribasso. (Non ero uno degli elettori.) La tecnica ha merito, però. Sarebbe stato meglio se srborlongan avesse pubblicato la sua risposta. Questo non è ancora successo, e non volevo che la tecnica si perdesse nelle nebbie della storia di modifica rifiutata da StackOverflow, quindi ho deciso di farla emergere come risposta separata.

Fondamentalmente la tecnica consiste nell’usare alcuni dei metodi Optional in modo intelligente per evitare di dover utilizzare un operatore ternario ( ? : 🙂 O un’istruzione if / else.

Il mio esempio inline verrebbe riscritto in questo modo:

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

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

 /** * Turns an Optional into a Stream of length zero or one depending upon * whether a value is present. */ static  Stream streamopt(Optional opt) { return opt.map(Stream::of) .orElseGet(Stream::empty); } Optional result = things.stream() .flatMap(t -> streamopt(resolve(t))) .findFirst(); 

COMMENTO

Confrontiamo le versioni originali vs quelle modificate direttamente:

 // 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 diretto, se a regola d’arte: otteniamo un Optional ; se ha un valore, restituiamo un stream che contiene quel valore e, se non ha valore, restituiamo un stream vuoto. Piuttosto semplice e facile da spiegare.

La modifica è intelligente e ha il vantaggio di evitare i condizionali. (So ​​che alcune persone non amano 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 . Quindi chiama Optional.map che è definito come segue:

Se è presente un valore, applicare la funzione di mapping fornita e, se il risultato è non null, restituire un Facoltativo che descrive il risultato. In caso contrario, restituire un Opzionale vuoto.

La chiamata della map(Stream::of) restituisce un Optional> . Se nell’ingresso era presente un valore Opzionale, l’Opzionale restituito contiene un stream che contiene il singolo risultato Altro. 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 . Se il suo valore di input è presente, ottiene il valore, che è lo Stream elemento singolo Stream . Altrimenti (se il valore di input è assente) restituisce uno Stream vuoto Stream . Quindi il risultato è corretto, lo stesso del codice condizionale originale.

Nei commenti che discutevano sulla mia risposta, riguardo alla modifica rifiutata, avevo descritto questa tecnica come “più concisa ma anche più oscura”. Mi tengo presente. Mi ci è voluto un po ‘per capire cosa stava facendo, e mi ci è voluto un po’ per scrivere la descrizione sopra di quello che stava facendo. La sottigliezza della chiave è la trasformazione da Optional a Optional> . Una volta che lo fai, ha senso, ma non era ovvio per me.

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

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

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

Si sostiene che non si desidera .filter(Optional::isPresent) e .map(Optional::get) .

Questo problema è stato risolto dal metodo @StuartMarks, tuttavia come risultato ora lo si mappa su un Optional , quindi ora è necessario utilizzare .flatMap(this::streamopt) e un get() alla fine.

Quindi consiste ancora di due affermazioni e ora puoi ottenere delle eccezioni con il nuovo metodo! Perché, cosa succede se ogni optional è vuoto? 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 realizzare ciò che vuoi, e questo è che vuoi salvare il risultato come T , non come un Optional .

Mi sono preso la libertà di creare una CustomOptional Optional e fornisce un metodo extra, flatStream() . Nota che non puoi estendere Optional :

 class CustomOptional { private final Optional optional; private CustomOptional() { this.optional = Optional.empty(); } private CustomOptional(final T value) { this.optional = Optional.of(value); } private CustomOptional(final Optional optional) { this.optional = optional; } public Optional getOptional() { return optional; } public static  CustomOptional empty() { return new CustomOptional<>(); } public static  CustomOptional of(final T value) { return new CustomOptional<>(value); } public static  CustomOptional 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 consumer) { optional.ifPresent(consumer); } public CustomOptional filter(final Predicate predicate) { return new CustomOptional<>(optional.filter(predicate)); } public  CustomOptional map(final Function mapper) { return new CustomOptional<>(optional.map(mapper)); } public  CustomOptional flatMap(final Function> 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 other) { return optional.orElseGet(other); } public  T orElseThrow(final Supplier exceptionSuppier) throws X { return optional.orElseThrow(exceptionSuppier); } public Stream 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 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 ancora restituire un Stream qui, poiché non puoi restituire T , perché se !optional.isPresent() , quindi T == null se lo dichiari tale, ma poi il tuo .flatMap(CustomOptional::flatStream) tenterebbe di aggiungere null a uno stream e ciò non è ansible.

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

Verrà ora NullPointerException una NullPointerException all’interno delle operazioni di streaming.

Conclusione

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

Una versione leggermente più breve che utilizza reduce :

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

È anche ansible spostare la funzione di riduzione su un metodo di utilità statico e quindi diventa:

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

Visto che la mia precedente risposta sembrava non essere molto popolare, darò un’altra possibilità.

Una risposta breve:

Sei principalmente sulla buona strada. Il codice più breve per ottenere l’output desiderato che potrei ricavare è questo:

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

Questo si adatta a tutte le tue esigenze:

  1. Troverà la prima risposta che si risolve in un Optional non vuoto
  2. Chiama this::resolve pigramente se necessario
  3. this::resolve non sarà chiamato dopo il primo risultato non vuoto
  4. Restituirà Optional

Risposta più lunga

L’unica modifica rispetto alla versione iniziale OP era che ho rimosso .map(Optional::get) prima di chiamare a .findFirst() e .flatMap(o -> o) aggiunto .flatMap(o -> o) come ultima chiamata nella catena.

Questo ha un buon effetto di eliminare il doppio facoltativo, ogni volta che lo stream trova un risultato reale.

Non puoi davvero andare più breve di questo in Java.

Lo snippet alternativo di codice che utilizza la tecnica più convenzionale for ciclo sarà circa lo stesso numero di righe di codice e avrà più o meno lo stesso ordine e il numero di operazioni che è necessario eseguire:

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

Solo 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 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 linee extra per il debug e verifica che solo il numero di chiamate da risolvere sia necessario …)

Eseguendolo su una riga di comando, ho ottenuto i seguenti risultati:

 $ java StackOferflow ab 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 

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

Viene fornito con una libreria di raccolta completa e immutabile che è molto simile a quella conosciuta da Scala. Queste collezioni sostituiscono le collezioni Java e Java 8’s Stream. Ha anche la propria implementazione di Option.

 import javaslang.collection.Stream; import javaslang.control.Option; Stream> options = Stream.of(Option.some("foo"), Option.none(), Option.some("bar")); // = Stream("foo", "bar") Stream 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 things = Stream.of(new Thing(1), new Thing(2), new Thing(3)); // = Some(Other(2)) Option others = things.flatMap(this::resolve).headOption(); } Option 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.

Null è supportato dal stream fornito My library AbacusUtil . Ecco il codice:

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

In ritardo alla festa, ma che dire

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

Puoi eliminare l’ultimo get () se crei un metodo util per convertire facoltativamente lo streaming manualmente:

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

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

Molto probabilmente stai sbagliando.

Java 8 Opzionale non è pensato per essere utilizzato in questo modo. Solitamente è riservato solo alle operazioni di terminal stream che possono o meno restituire un valore, come ad esempio trovare.

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

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

La regola generale è che dovresti cercare di ridurre il numero di elementi nello stream prima di trasformarli in qualcos’altro. YMMV naturalmente.