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