Commande de flux en Java
1. Vue d'ensemble
Dans ce didacticiel, nous allons vous expliquer commentdifferent uses of the Java Stream API affect the order in which a stream generates, processes, and collects data.
Nous verrons également commentordering influences performance.
2. Ordre de rencontre
En termes simples,encounter order est the order in which a Stream encounters data.
2.1. Ordre de rencontre des sourcesCollection
LesCollection que nous choisissons comme source affectent l'ordre de rencontre desStream.
Pour tester cela, créons simplement deux flux.
Notre premier est créé à partir d'unList, qui a un ordre intrinsèque.
Notre deuxième est créé à partir d'unTreeSet qui ne le fait pas.
Nous collectons ensuite la sortie de chaqueStream dans unArray pour comparer les résultats.
@Test
public void givenTwoCollections_whenStreamedSequentially_thenCheckOutputDifferent() {
List list = Arrays.asList("B", "A", "C", "D", "F");
Set set = new TreeSet<>(list);
Object[] listOutput = list.stream().toArray();
Object[] setOutput = set.stream().toArray();
assertEquals("[B, A, C, D, F]", Arrays.toString(listOutput));
assertEquals("[A, B, C, D, F]", Arrays.toString(setOutput));
}
Comme nous pouvons le voir dans notre exemple, leTreeSet n'a pas gardé l'ordre de notre séquence d'entrée, donc brouillant l'ordre de rencontre desStream.
Si notreStream est commandé, ildoesn’t matter whether our data is being processed sequentially or in parallel; l'implémentation conservera l'ordre de rencontre desStream.
Lorsque nous répétons notre test en utilisant des flux parallèles, nous obtenons le même résultat:
@Test
public void givenTwoCollections_whenStreamedInParallel_thenCheckOutputDifferent() {
List list = Arrays.asList("B", "A", "C", "D", "F");
Set set = new TreeSet<>(list);
Object[] listOutput = list.stream().parallel().toArray();
Object[] setOutput = set.stream().parallel().toArray();
assertEquals("[B, A, C, D, F]", Arrays.toString(listOutput));
assertEquals("[A, B, C, D, F]", Arrays.toString(setOutput));
}
2.2. Suppression de la commande
À tout moment, nous pouvonsexplicitly remove the order constraint with the unordered method.
Par exemple, déclarons unTreeSet:
Set set = new TreeSet<>(
Arrays.asList(-9, -5, -4, -2, 1, 2, 4, 5, 7, 9, 12, 13, 16, 29, 23, 34, 57, 102, 230));
Et si nous diffusons sans appelerunordered:
set.stream().parallel().limit(5).toArray();
L'ordre naturel deTreeSet est alors préservé:
[-9, -5, -4, -2, 1]
Mais, si nous supprimons explicitement la commande:
set.stream().unordered().parallel().limit(5).toArray();
Alors la sortie est différente:
[1, 4, 7, 9, 23]
La raison est double: Premièrement, comme les flux séquentiels traitent les données un élément à la fois,unordered n'a que peu d'effet en soi. Cependant, lorsque nous avons appeléparallel, nous avons également affecté la sortie.
3. Opérations intermédiaires
On peut aussiaffect stream ordering through intermediate operations.
Alors que la plupart des opérations intermédiaires maintiendront l'ordre desStream,, certaines le changeront de par leur nature.
Par exemple, nous pouvons affecter l’ordre des flux en triant:
@Test
public void givenUnsortedStreamInput_whenStreamSorted_thenCheckOrderChanged() {
List list = Arrays.asList(-3, 10, -4, 1, 3);
Object[] listOutput = list.stream().toArray();
Object[] listOutputSorted = list.stream().sorted().toArray();
assertEquals("[-3, 10, -4, 1, 3]", Arrays.toString(listOutput));
assertEquals("[-4, -3, 1, 3, 10]", Arrays.toString(listOutputSorted));
}
4. Opérations terminales
Enfin, nous pouvons affecter l'ordredepending on the terminal operation that we use.
4.1. ForEach vsForEachOrdered
ForEach andForEachOrdered peut sembler fournir la même fonctionnalité, mais ils ont une différence clé:ForEachOrdered guarantees to maintain the order of the Stream.
Si nous déclarons une liste:
List list = Arrays.asList("B", "A", "C", "D", "F");
Et utilisezforEachOrdered après la mise en parallèle:
list.stream().parallel().forEachOrdered(e -> logger.log(Level.INFO, e));
Ensuite, la sortie est ordonnée:
INFO: B
INFO: A
INFO: C
INFO: D
INFO: F
Cependant, si nous utilisonsforEach:
list.stream().parallel().forEach(e -> logger.log(Level.INFO, e));
Alors la sortie estunordered:
INFO: C
INFO: F
INFO: B
INFO: D
INFO: A
ForEach enregistre les éléments dans l'ordre dans lequel ils arrivent de chaque thread. Le deuxièmeStream with sesForEachOrdered method waits for each previous thread to complete avant d'appeler la méthodelog .
4.2. Collect
Lorsque nous utilisons la méthodecollect pour agréger la sortieStream , il est important de noter que lesCollection que nous choisissons auront un impact sur la commande.
Par exemple, la sortieinherently unordered Collections such as TreeSet won’t obey the order of the Stream:
@Test
public void givenSameCollection_whenStreamCollected_checkOutput() {
List list = Arrays.asList("B", "A", "C", "D", "F");
List collectionList = list.stream().parallel().collect(Collectors.toList());
Set collectionSet = list.stream().parallel()
.collect(Collectors.toCollection(TreeSet::new));
assertEquals("[B, A, C, D, F]", collectionList.toString());
assertEquals("[A, B, C, D, F]", collectionSet.toString());
}
Lors de l'exécution de notre code, nous voyons que l'ordre de nosStream change en se rassemblant dans unSet.
4.3. Spécification deCollections
Dans le cas où nous collectons vers une collection non ordonnée en utilisant, disons,Collectors.toMap, nous pouvons toujours appliquer l'ordre parchanging the implementation of our Collectors methods to use the Linked implementation.
Tout d'abord, nous initialiserons notre liste, avec les2-parameter version habituels de la méthodetoMap:
@Test
public void givenList_whenStreamCollectedToHashMap_thenCheckOrderChanged() {
List list = Arrays.asList("A", "BB", "CCC");
Map hashMap = list.stream().collect(Collectors
.toMap(Function.identity(), String::length));
Object[] keySet = hashMap.keySet().toArray();
assertEquals("[BB, A, CCC]", Arrays.toString(keySet));
}
Comme prévu, notre nouveauHashMap n’a pas conservé l’ordre d’origine de la liste d’entrée, mais changeons cela.
Avec notre deuxièmeStream, nous utiliserons le4-parameter version de la méthodetoMap pour dire à notresupplier de fournir un nouveauLinkedHashMap:
@Test
public void givenList_whenCollectedtoLinkedHashMap_thenCheckOrderMaintained(){
List list = Arrays.asList("A", "BB", "CCC");
Map linkedHashMap = list.stream().collect(Collectors.toMap(
Function.identity(),
String::length,
(u, v) -> u,
LinkedHashMap::new
));
Object[] keySet = linkedHashMap.keySet().toArray();
assertEquals("[A, BB, CCC]", Arrays.toString(keySet));
}
Hé, c’est beaucoup mieux!
Nous avons réussi à conserver l’ordre d’origine de la liste en collectant nos données vers unLinkedHashMap.
5. Performance
Si nous utilisons des flux séquentiels, la présence ou l’absence d’ordre ne fait guère de différence sur les performances de notre programme. Parallel streams, however, can be heavily affected by the presence of an ordered Stream.
La raison en est que chaque thread doit attendre le calcul de l'élément précédent desStream.
Essayons de le démontrer en utilisant lesJava Microbenchmark harness, JMH, pour mesurer les performances.
Dans les exemples suivants, nous mesurerons le coût des performances du traitement des flux parallèles ordonnés et non ordonnés avec certaines opérations intermédiaires courantes.
5.1. Distinct
Configurons un test à l'aide de la fonctiondistinct sur les flux ordonnés et non ordonnés.
@Benchmark
public void givenOrderedStreamInput_whenStreamDistinct_thenShowOpsPerMS() {
IntStream.range(1, 1_000_000).parallel().distinct().toArray();
}
@Benchmark
public void givenUnorderedStreamInput_whenStreamDistinct_thenShowOpsPerMS() {
IntStream.range(1, 1_000_000).unordered().parallel().distinct().toArray();
}
Lorsque nous touchons run, nous pouvons voir la disparité dans le temps pris par opération:
Benchmark Mode Cnt Score Error Units
TestBenchmark.givenOrdered... avgt 2 222252.283 us/op
TestBenchmark.givenUnordered... avgt 2 78221.357 us/op
5.2. Filter
Ensuite, nous utiliserons unStream parallèle avec une simple méthodefilter pour renvoyer tous les 10 entiers:
@Benchmark
public void givenOrderedStreamInput_whenStreamFiltered_thenShowOpsPerMS() {
IntStream.range(1, 100_000_000).parallel().filter(i -> i % 10 == 0).toArray();
}
@Benchmark
public void givenUnorderedStreamInput_whenStreamFiltered_thenShowOpsPerMS(){
IntStream.range(1,100_000_000).unordered().parallel().filter(i -> i % 10 == 0).toArray();
}
Fait intéressant, la différence entre nos deux flux est bien moindre que lors de l'utilisation de la méthodedistinct .
Benchmark Mode Cnt Score Error Units
TestBenchmark.givenOrdered... avgt 2 116333.431 us/op
TestBenchmark.givenUnordered... avgt 2 111471.676 us/op
6. Conclusion
Dans cet article, nous avons examiné le classement des flux, en nous concentrant sur lesthe different stages of the Stream process and how each one has its own effect.
Enfin, nous avons vu comment lesorder contract placed on a Stream can affect the performance of parallel streams.
Comme toujours, consultez l'ensemble d'échantillons completover on GitHub.