Copiez un flux pour éviter que "le flux a déjà été exploité ou fermé"


121

Je voudrais dupliquer un flux Java 8 pour pouvoir le gérer deux fois. Je peux collectcomme 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 y avoir un moyen plus efficace / élégant.

Existe-t-il un moyen de copier le flux sans le transformer en collection?

Je travaille en fait avec un flux de Eithers, je veux donc traiter la projection de gauche dans un sens avant de passer à la projection de droite et de traiter cette autre manière. Un peu comme ça (avec lequel, jusqu'à présent, je suis obligé d'utiliser l' toListastuce).

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

Pourriez-vous nous en dire plus sur le "processus à sens unique" ... consommez-vous les objets? Les cartographier? partitionBy () et groupingBy () peuvent vous amener directement à plus de 2 listes, mais vous pourriez bénéficier du mappage en premier ou simplement d'avoir une fourchette de décision dans votre forEach ().
AjahnCharles

Dans certains cas, le transformer en une collection ne peut pas être une option si nous avons affaire à un flux infini. Vous pouvez trouver une alternative pour la mémorisation ici: dzone.com/articles/how-to-replay-java-streams
Miguel Gamboa

Réponses:


88

Je pense que votre hypothèse sur l'efficacité est un peu rétrograde. Vous obtenez ce gain d'efficacité énorme 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 en boucle» qui vous permettent de faire circuler efficacement toutes les 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. S'il se trouve déjà dans une collection, tant mieux; puis l'itérer deux fois n'est pas cher.

Nous avons expérimenté dans la conception avec des «flux fourchus». Ce que nous avons constaté, c'est que soutenir cela avait des coûts réels; il alourdissait le cas courant (utilisation unique) aux dépens du cas rare. Le gros problème était de savoir «ce qui se passe lorsque les deux pipelines ne consomment pas de 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 les mêmes données à plusieurs reprises, stockez-les ou structurez vos opérations en consommateurs et procédez comme suit:

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

Vous pouvez également regarder dans la bibliothèque RxJava, car son modèle de traitement se prête mieux à ce type de "stream forking".


1
Peut-être que je n'aurais pas dû utiliser «efficacité», je comprends en quelque sorte pourquoi je me soucierais des flux (et ne rien stocker) si tout ce que je fais est de stocker immédiatement les données ( toList) pour pouvoir les traiter (le Eithercas étant l'exemple)?
Toby

11
Les flux sont à la fois expressifs et efficaces . Ils sont expressifs dans la mesure où ils vous permettent de configurer des opérations d'agrégation complexes sans beaucoup de détails accidentels (par exemple, des résultats intermédiaires) dans la manière de lire le code. Ils sont également efficaces, en ce sens qu'ils effectuent (généralement) un seul passage sur les données et ne remplissent pas les conteneurs de résultats intermédiaires. Ces deux propriétés réunies en font un modèle de programmation attractif pour de nombreuses situations. Bien entendu, tous les modèles de programmation ne répondent pas à tous les problèmes; vous devez encore décider si vous utilisez un outil approprié pour le travail.
Brian Goetz

1
Mais l'impossibilité de réutiliser un flux provoque des situations où le développeur est obligé de stocker des résultats intermédiaires (collecte) afin de traiter un flux de deux manières différentes. L'implication que le flux est généré plus d'une fois (à moins que vous ne le collectiez) semble claire - sinon vous n'auriez pas besoin d'une méthode de collecte.
Niall Connaughton

@NiallConnaughton Je ne suis pas sûr que ce soit votre point de vue. Si vous voulez le traverser deux fois, quelqu'un doit le stocker, ou vous devez le régénérer. Suggérez-vous que la bibliothèque devrait la mettre en mémoire tampon au cas où quelqu'un en aurait besoin deux fois? Ce serait idiot.
Brian Goetz

Ne suggérant pas que la bibliothèque devrait le mettre en mémoire tampon, mais en disant qu'en ayant des flux uniques, cela oblige les personnes qui veulent réutiliser un flux d'amorçage (c'est-à-dire: partager la logique déclarative utilisée pour le définir) à construire plusieurs flux dérivés pour soit collecter le flux d'amorçage, ou avoir accès à une fabrique de fournisseurs qui créera un double du flux d'amorçage. Les deux options ont leurs points faibles. Cette réponse contient beaucoup plus de détails sur le sujet: stackoverflow.com/a/28513908/114200 .
Niall Connaughton

73

Vous pouvez utiliser une variable locale avec un Supplier pour configurer les parties communes du pipeline de flux.

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 une opération de 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

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)

Pour surmonter cette limitation, nous devons créer une nouvelle chaîne de flux pour chaque opération de terminal que nous voulons 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 sauvegardés pour appeler l'opération de terminal souhaitée.


2
solution agréable et élégante. beaucoup plus java8-ish que la solution la plus votée.
dylaniato

Juste une note sur l'utilisation Suppliersi le Streamest construit de manière «coûteuse», vous payez ce coût pour chaque appel àSupplier.get() . ie si une requête de base de données ... cette requête est faite à chaque fois
Julien

Vous ne pouvez pas sembler suivre ce modèle après un mapTo en utilisant un IntStream. J'ai trouvé que je devais le reconvertir en Set<Integer>utilisation collect(Collectors.toSet())... et faire quelques opérations là-dessus. Je voulais max()et si une valeur spécifique était définie en deux opérations ...filter(d -> d == -1).count() == 1;
JGFMK

16

Utilisez a Supplierpour produire le flux pour chaque opération de terminaison.

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

Chaque fois que vous avez besoin d'un flux de cette collection, utilisez streamSupplier.get()pour obtenir un nouveau flux.

Exemples:

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

Je vous conseille car vous êtes le premier à avoir indiqué les fournisseurs ici.
EnzoBnl

9

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

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

En interne, il existe un tampon stockant toutes les valeurs qui ont été consommées à partir d'un flux mais pas de l'autre. C'est probablement aussi efficace que si vos deux flux sont consommés à peu près au même rythme, et si vous pouvez vivre avec le manque 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

Tuple2est probablement comme votre Pairtype alors Seqest Streamavec quelques améliorations.


2
Cette solution n'est pas thread-safe: vous ne pouvez pas passer l'un des flux à un autre thread. Je ne vois vraiment aucun scénario où les deux flux peuvent être consommés à un taux égal dans un seul thread et que vous avez en fait besoin de deux flux distincts. Si vous souhaitez produire deux résultats à partir du même flux, il serait préférable d'utiliser des collecteurs combinés (que vous avez déjà dans JOOL).
Tagir Valeev

@TagirValeev: Vous avez raison sur la sécurité des threads, bon point. Comment cela pourrait-il être fait en combinant des collectionneurs?
Lukas Eder

1
Je veux dire que si quelqu'un veut utiliser le même flux deux fois comme ça Tuple2<Seq<A>>, Seq<A>> t = duplicate(stream); long count = t.collect(counting()); List<A> list = t.collect(toList());, c'est mieux de le faire Tuple2<Long, List<A>> t = stream.collect(Tuple.collectors(counting(), toList()));. L'utilisation de l' Collectors.mapping/reducingun peut exprimer d'autres opérations de flux comme des collecteurs et des éléments de processus de manière assez différente, créant un seul tuple résultant. Donc, en général, vous pouvez faire beaucoup de choses en consommant le flux une fois sans duplication et il sera compatible avec le parallèle.
Tagir Valeev

2
Dans ce cas, vous réduirez toujours un flux après l'autre. Il ne sert donc à rien de rendre la vie plus difficile en introduisant l'itérateur sophistiqué qui rassemblera de toute façon tout le flux dans la liste sous le capot. Vous pouvez simplement collecter explicitement dans la liste, puis créer deux flux à partir de celle-ci comme OP l'indique (c'est le même nombre de lignes de code). Eh bien, vous n'aurez peut-être une amélioration que si la première réduction est un court-circuit, mais ce n'est pas le cas OP.
Tagir Valeev

1
@maaartinus: Merci, bon pointeur. J'ai créé un problème pour le benchmark. Je l'ai utilisé pour l' API offer()/ poll(), mais cela ArrayDequepourrait faire la même chose.
Lukas Eder

7

Vous pouvez créer un flux d'exécutables (par exemple):

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

failureet successsont les opérations à appliquer. Cela créera cependant un certain nombre d'objets temporaires et ne sera peut-être pas plus efficace que de partir d'une collection et de la diffuser / itérer deux fois.


4

Une autre façon de gérer 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 enchaî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()));

Il semble que peek ne soit pas censé être utilisé pour cela (voir softwareengineering.stackexchange.com/a/308979/195787 )
HectorJ

2
@HectorJ L'autre fil de discussion concerne la modification des éléments. J'ai supposé que ce n'était pas fait ici.
Martin

2

cyclops-react , une bibliothèque à laquelle je contribue, a une méthode statique qui vous permettra de dupliquer un Stream (et renvoie 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 Streamable (paresseuse) 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 remplira paresseusement sa collection de sauvegarde, de manière à pouvoir être partagé entre les threads. Streamable.fromStream (stream) n'entraînera aucune surcharge de synchronisation.


2
Et, bien sûr, il convient de noter que les flux résultants ont une surcharge CPU / mémoire importante et des performances parallèles très médiocres. De plus, cette solution n'est pas sûre pour les threads (vous ne pouvez pas passer l'un des flux résultants à un autre thread et le traiter en toute sécurité en parallèle). Ce serait beaucoup plus performant et sûr List<Integer> list = stream.collect(Collectors.toList()); streams = new Tuple2<>(list.stream(), list.stream())(comme le suggère OP). Veuillez également indiquer explicitement dans la réponse que vous êtes l'auteur de cyclop-streams. Lisez ceci .
Tagir Valeev

Mis à jour pour refléter que je suis l'auteur. Également un bon point pour discuter des caractéristiques de performance de chacun. Votre évaluation ci-dessus est à peu près parfaite pour StreamUtils.duplicate. StreamUtils.duplicate fonctionne en mettant en mémoire tampon les données d'un Stream à l'autre, ce qui entraîne à la fois une surcharge du processeur et de la mémoire (selon le cas d'utilisation). Cependant, pour Streamable.of (1,2,3), un nouveau Stream est créé directement à partir de la baie à chaque fois et les caractéristiques de performance, y compris les performances parallèles, seront les mêmes que pour Stream normalement créé.
John McClean

En outre, il existe une classe AsStreamable qui permet la création d'une instance Streamable à partir d'un Stream mais synchronise l'accès à la collection soutenant le Streamable lors de sa création (AsStreamable.synchronizedFromStream). Le rendant plus approprié pour une utilisation entre les threads (si c'est ce dont vous avez besoin - j'imagine 99% du temps que les flux sont créés et réutilisés sur le même thread).
John McClean

Salut Tagir - ne devriez-vous pas également indiquer dans votre commentaire que vous êtes l'auteur d'une bibliothèque concurrente?
John McClean

1
Les commentaires ne sont pas des réponses et je n'annonce pas ma bibliothèque ici car ma bibliothèque n'a pas de fonctionnalité pour dupliquer le flux (juste parce que je pense que c'est inutile), donc nous ne sommes pas en concurrence ici. Bien sûr, lorsque je propose une solution impliquant ma bibliothèque, je dis toujours explicitement que je suis l'auteur.
Tagir Valeev

0

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

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

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

Cas d'utilisation

Disons 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 / table des employés [Ceci est juste 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

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: a List, un tableau et a Stream.Builder. J'ai écrit un petit programme de référence, qui suggérait que du point de vue de la performance, il Listétait environ 30% plus lent que les deux autres qui étaient assez similaires.

Le seul inconvénient de la conversion en tableau est qu'il est délicat si votre type d'élément est un type générique (ce qui dans mon cas c'était); donc je préfère utiliser un Stream.Builder.

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

Je peux ensuite faire une copie de n'importe quel flux stren faisant str.collect(copyCollector())ce qui me semble tout à fait conforme à l'utilisation idiomatique des flux.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.