Ordenação de fluxo em Java

Ordenação de fluxo em Java

1. Visão geral

Neste tutorial, vamos nos aprofundar em comodifferent uses of the Java Stream API affect the order in which a stream generates, processes, and collects data.

Também veremos comoordering influences performance.

2. Ordem de encontro

Simplificando,encounter order é the order in which a Stream encounters data.

2.1. Ordem de encontro de fontes deCollection

OCollection que escolhemos como nossa fonte afeta a ordem do encontro deStream.

Para testar isso, vamos simplesmente criar dois fluxos.

Nosso primeiro é criado a partir de aList, que possui uma ordem intrínseca.

Nosso segundo é criado a partir de umTreeSet que não.

Em seguida, coletamos a saída de cadaStream em umArray para comparar os resultados.

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

Como podemos ver em nosso exemplo, oTreeSet  não manteve a ordem de nossa sequência de entrada, portanto, embaralhando a ordem de encontro deStream.

Se nossoStream for ordenado, serádoesn’t matter whether our data is being processed sequentially or in parallel; a implementação manterá a ordem de encontro deStream.

Quando repetimos nosso teste usando fluxos paralelos, obtemos o mesmo resultado:

@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. Removendo pedido

Em qualquer ponto, podemosexplicitly remove the order constraint with the unordered method.

Por exemplo, vamos declarar umTreeSet:

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

E se transmitirmos sem chamarunordered:

set.stream().parallel().limit(5).toArray();

Então, a ordem natural deTreeSet é preservada:

[-9, -5, -4, -2, 1]

Mas, se removermos explicitamente o pedido:

set.stream().unordered().parallel().limit(5).toArray();

Então a saída é diferente:

[1, 4, 7, 9, 23]

O motivo é duplo: primeiro, como os fluxos sequenciais processam os dados um elemento por vez,unordered  tem pouco efeito por si só. Quando chamamosparallel também, no entanto, afetamos a saída.

3. Operações Intermediárias

Também podemosaffect stream ordering through intermediate operations.

Embora a maioria das operações intermediárias mantenha a ordem deStream,, algumas, por sua natureza, a alteram.

Por exemplo, podemos afetar a ordem do fluxo classificando:

@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 são mais dois exemplos de operações intermediárias que irão, em última instância, mudar a ordem de umStream.

4. Operações de Terminal

Finalmente, podemos afetar a ordemdepending on the terminal operation that we use.

4.1. ForEach vsForEachOrdered

ForEach andForEachOrdered pode parecer fornecer a mesma funcionalidade, mas eles têm uma diferença fundamental:ForEachOrdered guarantees to maintain the order of the Stream.

Se declararmos uma lista:

List list = Arrays.asList("B", "A", "C", "D", "F");

E useforEachOrdered após paralelizar:

list.stream().parallel().forEachOrdered(e -> logger.log(Level.INFO, e));

Então a saída é ordenada:

INFO: B
INFO: A
INFO: C
INFO: D
INFO: F

No entanto, se usarmosforEach:

list.stream().parallel().forEach(e -> logger.log(Level.INFO, e));

Então, a saída éunordered:

INFO: C
INFO: F
INFO: B
INFO: D
INFO: A

ForEach registra os elementos na ordem em que chegam de cada thread. O segundoStream muda seuForEachOrdered method waits for each previous thread to complete antes de chamar o métodolog .

4.2. Collect

Quando usamos o métodocollect para agregar a saídaStream , é importante observar que osCollection que escolhermos afetarão o pedido.

Por exemplo, saídainherently 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());
}

Ao executar nosso código, vemos que a ordem de nossas mudançasStream coletando em umSet.

4.3. EspecificandoCollections

No caso de coletarmos para uma coleção não ordenada usando, digamos,Collectors.toMap, ainda podemos impor a ordenação porchanging the implementation of our Collectors methods to use the Linked implementation.

Primeiro, inicializaremos nossa lista, junto com o usual2-parameter version do métodotoMap:

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

Como esperado, nosso novoHashMap não manteve a ordem original da lista de entrada, mas vamos mudar isso.

Com nosso segundoStream, usaremos o4-parameter version do métodotoMap para dizer ao nossosupplier para fornecer um novoLinkedHashMap:

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

Ei, isso é muito melhor!

Conseguimos manter a ordem original da lista coletando nossos dados aLinkedHashMap.

5. atuação

Se estivermos usando streams sequenciais, a presença ou ausência de ordem faz pouca diferença para o desempenho de nosso programa. Parallel streams, however, can be heavily affected by the presence of an ordered Stream.

A razão para isso é que cada thread deve aguardar o cálculo do elemento anterior deStream.

Vamos tentar demonstrar isso usandoJava Microbenchmark harness, JMH, para medir o desempenho.

Nos exemplos a seguir, mediremos o custo de desempenho do processamento de fluxos paralelos ordenados e não ordenados com algumas operações intermediárias comuns.

5.1. Distinct

Vamos configurar um teste usando a funçãodistinct  em ambos os fluxos ordenados e não ordenados.

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

Quando pressionamos run, podemos ver a disparidade no tempo gasto por operação:

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 

A seguir, usaremos umStream paralelo com um métodofilter imples para retornar a cada 10: inteiro

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

Curiosamente, a diferença entre nossos dois fluxos é muito menor do que quando usamos o métododistinct .

Benchmark                        Mode  Cnt       Score   Error  Units
TestBenchmark.givenOrdered...    avgt    2  116333.431          us/op
TestBenchmark.givenUnordered...  avgt    2  111471.676          us/op

6. Conclusão

Neste artigo, examinamos a ordenação de fluxos, com foco emthe different stages of the Stream process and how each one has its own effect.

Finalmente, vimos como oorder contract placed on a Stream can affect the performance of parallel streams.

Como sempre, verifique o conjunto de amostra completoover on GitHub.