Parfois, je souhaite effectuer un ensemble d'opérations sur un flux, puis traiter le flux résultant de deux manières différentes avec d'autres opérations.
Puis-je faire cela sans avoir à spécifier deux fois les opérations initiales communes?
Par exemple, j'espère qu'une méthode dup()
telle que celle-ci existe:
Stream [] desired_streams = IntStream.range(1, 100).filter(n -> n % 2 == 0).dup();
Stream stream14 = desired_streams[0].filter(n -> n % 7 == 0); // multiples of 14
Stream stream10 = desired_streams[1].filter(n -> n % 5 == 0); // multiples of 10
Ce n'est pas possible en général.
Si vous souhaitez dupliquer un flux d'entrée ou un itérateur d'entrée, vous avez deux options:
List<>
Supposons que vous dupliquiez un flux en deux flux s1
et s2
. Si vous avez des éléments n1
avancés dans les éléments s1
et n2
avec s2
, vous devez conserver les éléments |n2 - n1|
en mémoire, pour suivre le rythme. Si votre flux est infini, il peut ne pas y avoir de limite supérieure pour le stockage requis.
Jetez un coup d’œil à tee()
pour voir ce qu’il faut:
Cet outil informatique peut nécessiter un stockage auxiliaire important (en fonction de la quantité de données temporaires à stocker). En général, si un itérateur utilise la plupart ou la totalité des données avant le démarrage d'un autre itérateur, il est plus rapide d'utiliser
list()
au lieu detee()
.
Pour que cette option fonctionne, vous aurez probablement besoin d'accéder au fonctionnement interne du flux. En d'autres termes, le générateur - la partie qui crée les éléments - devrait en premier lieu prendre en charge la copie. [OP: Voir ceci grande réponse , comme exemple de la façon dont cela peut être fait pour l'exemple de la question]
Cela ne fonctionnera pas à la saisie de l'utilisateur, car vous devrez copier l'état du "monde extérieur" dans son ensemble. La variable Stream
de Java ne prend pas en charge la copie, car elle est conçue pour être aussi générale que possible, spécialement pour travailler avec des fichiers, un réseau, un clavier, des capteurs, un caractère aléatoire, etc. Il ne peut pas être dupliqué sans stocker une copie des lectures]
Ce n'est pas seulement le cas en Java; c'est une règle générale. Vous pouvez constater que std::istream
en C++ ne prend en charge que la sémantique de déplacement, pas la sémantique de copie ("constructeur de copie (supprimé)"), pour cette raison (et d'autres).
Il n'est pas possible de dupliquer un flux de cette façon. Toutefois, vous pouvez éviter la duplication de code en déplaçant la partie commune dans une méthode ou une expression lambda.
Supplier<IntStream> supplier = () ->
IntStream.range(1, 100).filter(n -> n % 2 == 0);
supplier.get().filter(...);
supplier.get().filter(...);
C'est possible si vous mettez en mémoire tampon les éléments que vous avez consommés dans une copie, mais pas encore dans l'autre.
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<Integer>, Seq<Integer>> desired_streams = Seq.seq(
IntStream.range(1, 100).filter(n -> n % 2 == 0).boxed()
).duplicate();
(Remarque: nous devons actuellement encapsuler le flux, car nous n’avons pas encore implémenté IntSeq
)
En interne, il existe un tampon LinkedList
qui stocke toutes les valeurs qui ont été consommées d’un flux mais pas de l’autre. C'est probablement aussi efficace que possible si vos deux flux sont consommés à peu près au même taux.
Voici comment fonctionne l'algorithme:
static <T> Tuple2<Seq<T>, Seq<T>> duplicate(Stream<T> stream) {
final LinkedList<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()));
}
En fait, en utilisant jOOλ , vous pourrez écrire une ligne complète comme ceci:
Tuple2<Seq<Integer>, Seq<Integer>> desired_streams = Seq.seq(
IntStream.range(1, 100).filter(n -> n % 2 == 0).boxed()
).duplicate()
.map1(s -> s.filter(n -> n % 7 == 0))
.map2(s -> s.filter(n -> n % 5 == 0));
// This will yield 14, 28, 42, 56...
desired_streams.v1.forEach(System.out::println)
// This will yield 10, 20, 30, 40...
desired_streams.v2.forEach(System.out::println);
Vous pouvez également déplacer la génération de flux dans une méthode/fonction séparée qui renvoie ce flux et l'appeler deux fois.
Non plus,
Cela présente l’avantage d’être explicite sur ce que vous faites et fonctionne également pour des flux infinis.
Dans votre exemple:
final int[] arr = IntStream.range(1, 100).filter(n -> n % 2 == 0).toArray();
Ensuite
final IntStream s = IntStream.of(arr);
Mise à jour: This not work. Voir l'explication ci-dessous, après le texte de la réponse d'origine.
Quelle bêtise de moi. Tout ce que je dois faire c'est:
Stream desired_stream = IntStream.range(1, 100).filter(n -> n % 2 == 0);
Stream stream14 = desired_stream.filter(n -> n % 7 == 0); // multiples of 14
Stream stream10 = desired_stream.filter(n -> n % 5 == 0); // multiples of 10
Explication pourquoi cela ne fonctionne pas:
Si vous codez le code et essayez de collecter les deux flux, le premier fonctionnera correctement, mais essayer de diffuser le second lève l'exception: Java.lang.IllegalStateException: stream has already been operated upon or closed
.
Pour élaborer, les flux sont des objets avec état (qui ne peuvent pas être réinitialisés ou rembobinés). Vous pouvez les considérer comme des itérateurs, qui à leur tour sont comme des pointeurs. Donc, stream14
et stream10
peuvent être considérés comme des références au même pointeur. Consommer le premier flux jusqu'au bout fera en sorte que le pointeur passe "au-delà de la fin". Essayer de consommer le deuxième flux revient à essayer d'accéder à un pointeur qui est déjà "passé la fin", ce qui est naturellement une opération illégale.
Comme le montre la réponse acceptée, le code pour créer le flux doit être exécuté deux fois, mais il peut être compartimenté en une construction Supplier
lambda ou similaire.
Code de test complet: enregistrer dans Foo.Java
, puis javac Foo.Java
, puis Java Foo
import Java.util.stream.IntStream;
public class Foo {
public static void main (String [] args) {
IntStream s = IntStream.range(0, 100).filter(n -> n % 2 == 0);
IntStream s1 = s.filter(n -> n % 5 == 0);
s1.forEach(n -> System.out.println(n));
IntStream s2 = s.filter(n -> n % 7 == 0);
s2.forEach(n -> System.out.println(n));
}
}
Sortie:
$ javac Foo.Java
$ Java Foo
0
10
20
30
40
50
60
70
80
90
Exception in thread "main" Java.lang.IllegalStateException: stream has already been operated upon or closed
at Java.util.stream.AbstractPipeline.<init>(AbstractPipeline.Java:203)
at Java.util.stream.IntPipeline.<init>(IntPipeline.Java:91)
at Java.util.stream.IntPipeline$StatelessOp.<init>(IntPipeline.Java:592)
at Java.util.stream.IntPipeline$9.<init>(IntPipeline.Java:332)
at Java.util.stream.IntPipeline.filter(IntPipeline.Java:331)
at Foo.main(Foo.Java:8)
Pour les flux non infinis, si vous avez accès à la source, son plus simple:
@Test
public void testName() throws Exception {
List<Integer> integers = Arrays.asList(1, 2, 4, 5, 6, 7, 8, 9, 10);
Stream<Integer> stream1 = integers.stream();
Stream<Integer> stream2 = integers.stream();
stream1.forEach(System.out::println);
stream2.forEach(System.out::println);
}
empreintes
1 2 4 5 6 7 8 9 dix
1 2 4 5 6 7 8 9 dix
Pour votre cas:
Stream originalStream = IntStream.range(1, 100).filter(n -> n % 2 == 0)
List<Integer> listOf = originalStream.collect(Collectors.toList())
Stream stream14 = listOf.stream().filter(n -> n % 7 == 0);
Stream stream10 = listOf.stream().filter(n -> n % 5 == 0);
Pour des performances etc., lisez la réponse de quelqu'un d'autre;)