web-dev-qa-db-fra.com

Copier un flux pour éviter que "le flux ait déjà été traité ou fermé"

J'aimerais dupliquer un flux Java 8 afin de pouvoir le traiter deux fois. Je peux collect sous forme de liste et obtenir de nouveaux flux à partir de cela;

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

Mais je pense qu'il devrait exister une méthode plus efficace/élégante.

Est-il possible de copier le flux sans le transformer en une collection?

En fait, je travaille avec un flux de Eithers. Je souhaite donc traiter la projection de gauche d’une manière avant de passer à la projection de droite et d’en traiter d’une autre manière. Un peu comme ça (jusqu'à présent, je suis obligé d'utiliser l'astuce toList avec).

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 -> ... );
84
Toby

Je pense que votre hypothèse d’efficacité est plutôt rétrograde. Vous obtenez cette efficacité si vous n'utilisez les données qu'une seule fois, car vous n'avez pas à les stocker, et les flux vous offrent de puissantes optimisations de "fusion de boucles" qui vous permettent de gérer efficacement l'ensemble des données dans le pipeline. 

Si vous souhaitez réutiliser les mêmes données, vous devez, par définition, les générer deux fois (de manière déterministe) ou les stocker. Si cela se trouve déjà dans une collection, tant mieux; puis le réitérer deux fois est bon marché. 

Nous avons expérimenté la conception avec des "flux fourchus". Ce que nous avons constaté, c’est que supporter cela avait des coûts réels; cela alourdissait le cas commun (utiliser une fois) au détriment du cas peu commun. Le gros problème était de savoir "ce qui se passe lorsque les deux pipelines ne consomment pas les données au même rythme". De toute façon, vous revenez à la mise en mémoire tampon. C'était une caractéristique qui ne portait clairement pas son poids. 

Si vous souhaitez utiliser plusieurs fois les mêmes données, stockez-les ou structurez-les en tant que consommateurs et procédez comme suit:

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

Vous pouvez également consulter la bibliothèque RxJava, car son modèle de traitement se prête mieux à ce type de "falsification de flux".

76
Brian Goetz

Utilisez Java.util.function.Supplier .

De http://winterbe.com/posts/2014/07/31/Java8-stream-tutorial-examples/ :

Réutilisation des flux

Les flux Java 8 ne peuvent pas être réutilisés. Dès que vous appelez un terminal, le flux est fermé:

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

L'appel de noneMatch après anyMatch sur le même flux entraîne l'exception suivante:

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)

Pour surmonter cette limitation, nous devons créer une nouvelle chaîne de flux pour chaque opération de terminal que nous souhaitons exécuter, par exemple. nous pourrions créer un fournisseur de flux pour construire un nouveau flux avec toutes les opérations intermédiaires déjà configurées:

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

Chaque appel à get() construit un nouveau flux sur lequel nous sommes enregistrés pour appeler l'opération de terminal souhaitée.

48
user4975679

Nous avons implémenté une méthode duplicate() pour les flux dans jOOλ , une bibliothèque Open Source créée pour améliorer les tests d'intégration de jOOQ . Essentiellement, vous pouvez simplement écrire:

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

En interne, il y a un tampon stockant toutes les valeurs qui ont été consommées d'un flux mais pas de l'autre. C’est probablement aussi efficace que cela peut se produire si vos deux flux sont consommés à peu près au même taux, et si vous pouvez vivre avec l’absence de sécurité des threads.

Voici comment fonctionne l'algorithme:

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

Plus de code source ici

Tuple2 est probablement comme votre type Pair, alors que Seq est Stream avec quelques améliorations.

8
Lukas Eder

Vous pouvez créer un flux de runnables (par exemple):

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

failure et success sont les opérations à appliquer. Cela créera cependant quelques objets temporaires et ne sera peut-être pas plus efficace que de partir d'une collection et de la diffuser/l'itérer deux fois.

7
assylias

Une autre façon de traiter les éléments plusieurs fois consiste à utiliser Stream.peek (Consumer) :

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

peek(Consumer) peut être chaîné autant de fois que nécessaire.

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

cyclops-react , une librairie à laquelle je contribue, a une méthode statique qui vous permettra de dupliquer un flux (et retourne un jOOλ tuple of Streams).

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

Voir les commentaires, il y a une pénalité de performance qui sera encourue lors de l'utilisation de dupliquer sur un flux existant. Une alternative plus performante serait d’utiliser Streamable: -

Il existe également une classe (paresseuse) Streamable qui peut être construite à partir d'un Stream, Iterable ou Array et rejouée plusieurs fois.

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

AsStreamable.synchronizedFromStream (stream) - peut être utilisé pour créer un Streamable qui va remplir paresseusement sa collection de sauvegarde, de manière à pouvoir être partagé entre plusieurs threads. Streamable.fromStream (stream) n'engendrera pas de temps système pour la synchronisation. 

2
John McClean

J'ai eu un problème similaire et je pouvais penser à trois structures intermédiaires différentes à partir desquelles créer une copie du flux: une List, un tableau et un Stream.Builder. J’ai écrit un petit programme de référence qui suggérait que, du point de vue de la performance, la List était environ 30% plus lente que les deux autres qui étaient assez similaires.

Le seul inconvénient de la conversion en tableau est que cela est délicat si votre type d'élément est un type générique (ce qui dans mon cas était le cas); par conséquent, je préfère utiliser un Stream.Builder.

J'ai fini par écrire une petite fonction qui crée une 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);
}

Je peux ensuite faire une copie de tout flux str en faisant str.collect(copyCollector()), ce qui est tout à fait en rapport avec l'utilisation idiomatique des flux. 

0
Jeremy Hicks

Pour ce problème particulier, vous pouvez également utiliser le partitionnement. Quelque chose comme

     // 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
Lubomir Varga

Nous pouvons utiliser Stream Builder au moment de la lecture ou de l'itération d'un flux ..__ Voici le document Stream Builder.

https://docs.Oracle.com/javase/8/docs/api/Java/util/stream/Stream.Builder.html

Cas d'utilisation

Supposons que nous avons un flux d'employés et que nous devons utiliser ce flux pour écrire les données des employés dans un fichier Excel, puis mettre à jour la collection/le tableau d'employés [Il s'agit simplement d'un cas d'utilisation pour montrer l'utilisation de 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
Lokesh Singal