Copia uno stream per evitare che "lo stream sia già stato operato o chiuso"


121

Vorrei duplicare uno stream Java 8 in modo da poterlo gestire due volte. Posso collectcome elenco e ottenere nuovi flussi da quello;

// doSomething() returns a stream
List<A> thing = doSomething().collect(toList());
thing.stream()... // do stuff
thing.stream()... // do other stuff

Ma penso che dovrebbe esserci un modo più efficiente / elegante.

C'è un modo per copiare lo stream senza trasformarlo in una raccolta?

In realtà sto lavorando con un flusso di Eithers, quindi voglio elaborare la proiezione sinistra in un modo prima di passare alla proiezione destra e affrontarla in un altro modo. Un po 'come questo (con cui, finora, sono costretto a usare il toListtrucco).

List<Either<Pair<A, Throwable>, A>> results = doSomething().collect(toList());

Stream<Pair<A, Throwable>> failures = results.stream().flatMap(either -> either.left());
failures.forEach(failure -> ... );

Stream<A> successes = results.stream().flatMap(either -> either.right());
successes.forEach(success -> ... );

Potresti approfondire di più sul "processo in un modo" ... stai consumando gli oggetti? Mappandoli? partitionBy () e groupingBy () possono portarti direttamente a 2+ elenchi, ma potresti trarre vantaggio dalla mappatura prima o semplicemente dall'avere un fork decisionale nel tuo forEach ().
AjahnCharles

In alcuni casi, trasformarlo in una raccolta potrebbe non essere un'opzione se abbiamo a che fare con un flusso infinito. Puoi trovare un'alternativa per la memorizzazione qui: dzone.com/articles/how-to-replay-java-streams
Miguel Gamboa

Risposte:


88

Penso che la tua ipotesi sull'efficienza sia al contrario. Otterrai questo enorme guadagno in termini di efficienza se utilizzerai i dati una sola volta, perché non è necessario archiviarli, e gli stream ti offrono potenti ottimizzazioni di "loop fusion" che ti consentono di fluire tutti i dati in modo efficiente attraverso la pipeline.

Se si desidera riutilizzare gli stessi dati, per definizione è necessario generarli due volte (in modo deterministico) o memorizzarli. Se capita già di essere in una collezione, bene; quindi iterarlo due volte è economico.

Abbiamo fatto esperimenti nel design con "flussi biforcuti". Quello che abbiamo scoperto è che sostenere questa operazione aveva costi reali; appesantiva il caso comune (uso una volta) a scapito del caso raro. Il grosso problema era affrontare "cosa succede quando le due pipeline non consumano i dati alla stessa velocità". Ora sei tornato comunque al buffering. Questa era una caratteristica che chiaramente non aveva il suo peso.

Se desideri operare ripetutamente sugli stessi dati, memorizzali o organizza le tue operazioni come Consumatori e procedi come segue:

stream()...stuff....forEach(e -> { consumerA(e); consumerB(e); });

Potresti anche esaminare la libreria RxJava, poiché il suo modello di elaborazione si presta meglio a questo tipo di "stream fork".


1
Forse non avrei dovuto usare "efficienza", sto cercando di capire perché dovrei preoccuparmi dei flussi (e non memorizzare nulla) se tutto ciò che faccio è memorizzare immediatamente i dati ( toList) per essere in grado di elaborarli (il Eithercaso essere l'esempio)?
Toby

11
I flussi sono sia espressivi che efficienti . Sono espressivi in ​​quanto consentono di impostare complesse operazioni di aggregazione senza molti dettagli accidentali (ad esempio, risultati intermedi) nel modo di leggere il codice. Sono anche efficienti, in quanto (generalmente) effettuano un singolo passaggio sui dati e non popolano contenitori di risultati intermedi. Queste due proprietà insieme le rendono un modello di programmazione interessante per molte situazioni. Naturalmente, non tutti i modelli di programmazione soddisfano tutti i problemi; devi ancora decidere se stai utilizzando uno strumento appropriato per il lavoro.
Brian Goetz

1
Ma l'incapacità di riutilizzare un flusso causa situazioni in cui lo sviluppatore è costretto a memorizzare risultati intermedi (raccolta) per elaborare un flusso in due modi diversi. L'implicazione che il flusso viene generato più di una volta (a meno che non lo raccogli) sembra chiara, perché altrimenti non avresti bisogno di un metodo di raccolta.
Niall Connaughton,

@NiallConnaughton Non sono sicuro che il tuo punto sia. Se vuoi percorrerlo due volte, qualcuno deve immagazzinarlo, oppure devi rigenerarlo. Stai suggerendo che la libreria dovrebbe bufferizzarlo nel caso qualcuno ne avesse bisogno due volte? Sarebbe stupido.
Brian Goetz

Non suggerendo che la libreria dovrebbe bufferizzarlo, ma dicendo che avendo flussi come una tantum, costringe le persone che vogliono riutilizzare un flusso seme (cioè: condividere la logica dichiarativa usata per definirlo) a costruire più flussi derivati ​​da raccogliere seed stream o avere accesso a una factory di provider che creerà un duplicato del seed stream. Entrambe le opzioni hanno i loro punti deboli. Questa risposta contiene molti più dettagli sull'argomento: stackoverflow.com/a/28513908/114200 .
Niall Connaughton,

73

È possibile utilizzare una variabile locale con a Supplierper impostare parti comuni della pipeline di flusso.

Da http://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples/ :

Riutilizzo dei flussi

I flussi Java 8 non possono essere riutilizzati. Non appena chiami un'operazione da terminale, il flusso viene chiuso:

Stream<String> stream = Stream.of("d2", "a2", "b1", "b3", "c")
    .filter(s -> s.startsWith("a"));
stream.anyMatch(s -> true);    // ok
stream.noneMatch(s -> true);   // exception

Calling `noneMatch` after `anyMatch` on the same stream results in the following exception:
java.lang.IllegalStateException: stream has already been operated upon or closed
at 
java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)
at 
java.util.stream.ReferencePipeline.noneMatch(ReferencePipeline.java:459)
at com.winterbe.java8.Streams5.test7(Streams5.java:38)
at com.winterbe.java8.Streams5.main(Streams5.java:28)

Per superare questa limitazione dobbiamo creare una nuova catena di flussi per ogni operazione terminale che vogliamo eseguire, ad esempio potremmo creare un fornitore di flussi per costruire un nuovo flusso con tutte le operazioni intermedie già impostate:

Supplier<Stream<String>> streamSupplier =
    () -> Stream.of("d2", "a2", "b1", "b3", "c")
            .filter(s -> s.startsWith("a"));

streamSupplier.get().anyMatch(s -> true);   // ok
streamSupplier.get().noneMatch(s -> true);  // ok

Ogni chiamata a get()costruisce un nuovo flusso su cui siamo salvati per chiamare l'operazione di terminale desiderata.


2
bella ed elegante soluzione. molto più java8 rispetto alla soluzione più votata.
dylania al

Solo una nota sull'utilizzo Supplierse il Streamè costruito con un modo "costoso", paghi quel costo per ogni chiamata aSupplier.get() . cioè se una query al database ... quella query viene eseguita ogni volta
Julien

Non riesci a seguire questo modello dopo un mapTo sebbene utilizzi un IntStream. Ho scoperto che dovevo riconvertirlo in un Set<Integer>utilizzo collect(Collectors.toSet())... e fare un paio di operazioni su questo. Volevo max()e se un valore specifico fosse impostato come due operazioni ...filter(d -> d == -1).count() == 1;
JGFMK

16

Utilizzare a Supplierper produrre il flusso per ciascuna operazione di terminazione.

Supplier<Stream<Integer>> streamSupplier = () -> list.stream();

Ogni volta che hai bisogno di uno stream di quella raccolta, usalo streamSupplier.get()per ottenere un nuovo stream.

Esempi:

  1. streamSupplier.get().anyMatch(predicate);
  2. streamSupplier.get().allMatch(predicate2);

Ti ho elogiato perché sei il primo ad aver indicato i fornitori qui.
EnzoBnl

9

Abbiamo implementato un duplicate()metodo per i flussi in jOOλ , una libreria Open Source che abbiamo creato per migliorare i test di integrazione per jOOQ . In sostanza, puoi semplicemente scrivere:

Tuple2<Seq<A>, Seq<A>> duplicates = Seq.seq(doSomething()).duplicate();

Internamente, è presente un buffer che memorizza tutti i valori che sono stati consumati da un flusso ma non dall'altro. Probabilmente è altrettanto efficiente se i tuoi due flussi vengono consumati alla stessa velocità e se puoi convivere con la mancanza di thread-safety .

Ecco come funziona l'algoritmo:

static <T> Tuple2<Seq<T>, Seq<T>> duplicate(Stream<T> stream) {
    final List<T> gap = new LinkedList<>();
    final Iterator<T> it = stream.iterator();

    @SuppressWarnings("unchecked")
    final Iterator<T>[] ahead = new Iterator[] { null };

    class Duplicate implements Iterator<T> {
        @Override
        public boolean hasNext() {
            if (ahead[0] == null || ahead[0] == this)
                return it.hasNext();

            return !gap.isEmpty();
        }

        @Override
        public T next() {
            if (ahead[0] == null)
                ahead[0] = this;

            if (ahead[0] == this) {
                T value = it.next();
                gap.offer(value);
                return value;
            }

            return gap.poll();
        }
    }

    return tuple(seq(new Duplicate()), seq(new Duplicate()));
}

Altro codice sorgente qui

Tuple2è probabilmente come il tuo Pairtipo, mentre Seqè Streamcon alcuni miglioramenti.


2
Questa soluzione non è thread-safe: non è possibile passare uno dei flussi a un altro thread. Non vedo davvero nessuno scenario in cui entrambi i flussi possono essere consumati alla stessa velocità in un singolo thread e in realtà sono necessari due flussi distinti. Se vuoi produrre due risultati dallo stesso flusso, sarebbe molto meglio usare i raccoglitori combinati (che hai già in JOOL).
Tagir Valeev

@TagirValeev: Hai ragione sulla sicurezza dei thread, buon punto. Come è possibile farlo combinando i collezionisti?
Lukas Eder

1
Voglio dire, se qualcuno vuole usare lo stesso flusso due volte in questo modo Tuple2<Seq<A>>, Seq<A>> t = duplicate(stream); long count = t.collect(counting()); List<A> list = t.collect(toList());, è meglio Tuple2<Long, List<A>> t = stream.collect(Tuple.collectors(counting(), toList()));. Usarne Collectors.mapping/reducinguno può esprimere altre operazioni di flusso come raccoglitori ed elementi di processo in modo abbastanza diverso creando una singola tupla risultante. Quindi in generale puoi fare molte cose consumando lo stream una volta senza duplicazioni e sarà compatibile con il parallelo.
Tagir Valeev

2
In questo caso ridurrai comunque un flusso dopo l'altro. Quindi non ha senso rendere la vita più difficile introducendo l'iteratore sofisticato che comunque raccoglierà l'intero flusso nella lista sotto il cofano. Puoi semplicemente raccogliere nell'elenco in modo esplicito, quindi creare due flussi da esso come dice OP (è lo stesso numero di righe di codice). Bene, potresti avere qualche miglioramento solo se la prima riduzione è in cortocircuito, ma non è il caso OP.
Tagir Valeev

1
@maaartinus: Grazie, buon puntatore. Ho creato un problema per il benchmark. L'ho usato per l' API offer()/ poll(), ma ArrayDequepotrebbe fare lo stesso.
Lukas Eder

7

Potresti creare un flusso di eseguibili (ad esempio):

results.stream()
    .flatMap(either -> Stream.<Runnable> of(
            () -> failure(either.left()),
            () -> success(either.right())))
    .forEach(Runnable::run);

Dove failuree successsono le operazioni da applicare. Ciò creerà tuttavia un bel po 'di oggetti temporanei e potrebbe non essere più efficiente dell'avvio da una raccolta e dello streaming / iterazione due volte.


4

Un altro modo per gestire gli elementi più volte è utilizzare Stream.peek (Consumer) :

doSomething().stream()
.peek(either -> handleFailure(either.left()))
.foreach(either -> handleSuccess(either.right()));

peek(Consumer) può essere concatenato tante volte quanto necessario.

doSomething().stream()
.peek(element -> handleFoo(element.foo()))
.peek(element -> handleBar(element.bar()))
.peek(element -> handleBaz(element.baz()))
.foreach(element-> handleQux(element.qux()));

Sembra che peek non debba essere usato per questo (vedi softwareengineering.stackexchange.com/a/308979/195787 )
HectorJ

2
@HectorJ L'altro thread riguarda la modifica degli elementi. Ho pensato che non fosse stato fatto qui.
Martin

2

cyclops-react , una libreria a cui contribuisco, ha un metodo statico che ti consentirà di duplicare uno Stream (e restituisce una jOOλ Tuple of Streams).

    Stream<Integer> stream = Stream.of(1,2,3);
    Tuple2<Stream<Integer>,Stream<Integer>> streams =  StreamUtils.duplicate(stream);

Vedere i commenti, è presente una penalizzazione delle prestazioni che si verificherà quando si utilizza il duplicato su uno stream esistente. Un'alternativa più performante sarebbe usare Streamable: -

Esiste anche una classe Streamable (pigra) che può essere costruita da Stream, Iterable o Array e riprodotta più volte.

    Streamable<Integer> streamable = Streamable.of(1,2,3);
    streamable.stream().forEach(System.out::println);
    streamable.stream().forEach(System.out::println);

AsStreamable.synchronizedFromStream (stream): può essere utilizzato per creare uno Streamable che popolerà pigramente la sua raccolta di backup, in modo tale da poter essere condiviso tra i thread. Streamable.fromStream (stream) non subirà alcun sovraccarico di sincronizzazione.


2
E, naturalmente, va notato che i flussi risultanti hanno un sovraccarico di CPU / memoria significativo e prestazioni parallele molto scarse. Anche questa soluzione non è thread-safe (non è possibile passare uno dei flussi risultanti a un altro thread ed elaborarlo in modo sicuro in parallelo). Sarebbe molto più performante e sicuro List<Integer> list = stream.collect(Collectors.toList()); streams = new Tuple2<>(list.stream(), list.stream())(come suggerisce OP). Inoltre, per favore, indica esplicitamente nella risposta che sei l'autore di cyclop-stream. Leggi questo .
Tagir Valeev

Aggiornato per riflettere che sono l'autore. Anche un buon punto per discutere le caratteristiche delle prestazioni di ciascuno. La tua valutazione sopra è praticamente azzeccata per StreamUtils.duplicate. StreamUtils.duplicate funziona eseguendo il buffering dei dati da un flusso all'altro, incorrendo in un overhead della CPU e della memoria (a seconda del caso d'uso). Per Streamable.of (1,2,3), tuttavia, un nuovo Stream viene creato ogni volta direttamente dall'array e le caratteristiche delle prestazioni, incluse le prestazioni parallele, saranno le stesse dello Stream normalmente creato.
John McClean

Inoltre, esiste una classe AsStreamable che consente la creazione di un'istanza Streamable da uno Stream ma sincronizza l'accesso alla raccolta che supporta lo Streamable quando viene creato (AsStreamable.synchronizedFromStream). Rendendolo più adatto per l'uso tra i thread (se questo è ciò di cui hai bisogno, immagino che il 99% delle volte gli stream vengano creati e riutilizzati sullo stesso thread).
John McClean

Ciao Tagir, non dovresti anche rivelare nel tuo commento che sei autore di una libreria concorrente?
John McClean

1
I commenti non sono risposte e non pubblicizzo la mia libreria qui poiché la mia libreria non ha la funzione di duplicare lo stream (solo perché penso che sia inutile), quindi non competiamo qui. Ovviamente quando propongo una soluzione che coinvolga la mia biblioteca dico sempre esplicitamente che sono l'autore.
Tagir Valeev

0

Per questo particolare problema puoi usare anche il partizionamento. Qualcosa di simile a

     // Partition Eighters into left and right
     List<Either<Pair<A, Throwable>, A>> results = doSomething();
     Map<Boolean, Object> passingFailing = results.collect(Collectors.partitioningBy(s -> s.isLeft()));
     passingFailing.get(true) <- here will be all passing (left values)
     passingFailing.get(false) <- here will be all failing (right values)

0

Possiamo utilizzare Stream Builder al momento della lettura o dell'iterazione di un flusso. Ecco il documento di Stream Builder .

https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.Builder.html

Caso d'uso

Supponiamo di avere un flusso di dipendenti e dobbiamo utilizzare questo flusso per scrivere i dati dei dipendenti nel file Excel e quindi aggiornare la raccolta / tabella dei dipendenti [Questo è solo un caso d'uso per mostrare l'uso di Stream Builder]:

Stream.Builder<Employee> builder = Stream.builder();

employee.forEach( emp -> {
   //store employee data to excel file 
   // and use the same object to build the stream.
   builder.add(emp);
});

//Now this stream can be used to update the employee collection
Stream<Employee> newStream = builder.build();

0

Avevo un problema simile e potevo pensare a tre diverse strutture intermedie da cui creare una copia del flusso: a List, un array e a Stream.Builder. Ho scritto un piccolo programma di benchmark, che suggeriva che dal punto di vista delle prestazioni Listera circa il 30% più lento degli altri due che erano abbastanza simili.

L'unico inconveniente della conversione in un array è che è complicato se il tuo tipo di elemento è un tipo generico (cosa che nel mio caso era); quindi preferisco usare un file Stream.Builder.

Ho finito per scrivere una piccola funzione che crea un Collector:

private static <T> Collector<T, Stream.Builder<T>, Stream<T>> copyCollector()
{
    return Collector.of(Stream::builder, Stream.Builder::add, (b1, b2) -> {
        b2.build().forEach(b1);
        return b1;
    }, Stream.Builder::build);
}

Posso quindi fare una copia di qualsiasi flusso strfacendo il str.collect(copyCollector())che sembra abbastanza in linea con l'uso idiomatico dei flussi.

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.