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 Either
s. 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 -> ... );
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".
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.
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()));
}
Tuple2
est probablement comme votre type Pair
, alors que Seq
est Stream
avec quelques améliorations.
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);
Où 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.
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()));
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.
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.
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)
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();