Commande de flux en Java

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

unordered andempty sont deux autres exemples d'opérations intermédiaires qui finiront par changer l'ordre d'unStream.

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.