Guia para Stream.reduce ()
1. Visão geral
OStream API fornece um rico repertório de funções intermediárias, de redução e de terminal, que também oferecem suporte à paralelização.
Mais especificamente,reduction stream operations allow us to produce one single result from a sequence of elements, aplicando repetidamente uma operação de combinação aos elementos na sequência.
Neste tutorial,we’ll look at the general-purpose Stream.reduce() operation e veja em alguns casos de uso concretos.
2. Os principais conceitos: identidade, acumulador e combinador
Antes de nos aprofundarmos no uso da operaçãoStream.reduce(), vamos dividir os elementos participantes da operação em blocos separados. Dessa forma, entenderemos mais facilmente a função que cada um desempenha:
-
Identity - um elemento que é o valor inicial da operação de redução e o resultado padrão se o fluxo estiver vazio
-
Accumulator - uma função que leva dois parâmetros: um resultado parcial da operação de redução e o próximo elemento do fluxo
-
Combiner - uma função usada para combinar o resultado parcial da operação de redução quando a redução é paralelizada, ou quando há uma incompatibilidade entre os tipos de argumentos do acumulador e os tipos de implementação do acumulador
3. UsandoStream.reduce()
Para entender melhor a funcionalidade dos elementos de identidade, acumulador e combinador, vejamos alguns exemplos básicos:
List numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
int result = numbers
.stream()
.reduce(0, (subtotal, element) -> subtotal + element);
assertThat(result).isEqualTo(21);
Neste caso,the Integer value 0 is the identity. Armazena o valor inicial da operação de redução, e também o resultado padrão quando o fluxo de valores deInteger está vazio.
Da mesma forma,the lambda expression:
subtotal, element -> subtotal + element
is the accumulator, uma vez que leva a soma parcial dos valores deInteger e o próximo elemento no fluxo.
Para tornar o código ainda mais conciso, podemos usar uma referência de método, em vez de uma expressão lambda:
int result = numbers.stream().reduce(0, Integer::sum);
assertThat(result).isEqualTo(21);
Claro, podemos usar uma operaçãoreduce() em streams que contêm outros tipos de elementos.
Por exemplo, podemos usarreduce() em uma matriz de elementosString e juntá-los em um único resultado:
List letters = Arrays.asList("a", "b", "c", "d", "e");
String result = letters
.stream()
.reduce("", (partialString, element) -> partialString + element);
assertThat(result).isEqualTo("abcde");
Da mesma forma, podemos mudar para a versão que usa uma referência de método:
String result = letters.stream().reduce("", String::concat);
assertThat(result).isEqualTo("abcde");
Vamos usar a operaçãoreduce() para juntar os elementos em letras maiúsculas da matrizletters:
String result = letters
.stream()
.reduce(
"", (partialString, element) -> partialString.toUpperCase() + element.toUpperCase());
assertThat(result).isEqualTo("ABCDE");
Além disso, podemos usarreduce() em um fluxo paralelizado (mais sobre isso mais tarde):
List ages = Arrays.asList(25, 30, 45, 28, 32);
int computedAges = ages.parallelStream().reduce(0, a, b -> a + b, Integer::sum);
Quando um fluxo é executado em paralelo, o tempo de execução Java divide o fluxo em vários substreams. Nesses casos,we need to use a function to combine the results of the substreams into a single one. This is the role of the combiner - no snippet acima, é a referência do métodoInteger::sum.
Engraçado, este código não compila:
List users = Arrays.asList(new User("John", 30), new User("Julie", 35));
int computedAges =
users.stream().reduce(0, (partialAgeResult, user) -> partialAgeResult + user.getAge());
Neste caso, temos um fluxo de objetosUser, e os tipos dos argumentos do acumulador sãoIntegereUser. No entanto, a implementação do acumulador é uma soma deIntegers,, então o compilador simplesmente não consegue inferir o tipo do parâmetrouser.
Podemos corrigir esse problema usando um combinador:
int result = users.stream()
.reduce(0, (partialAgeResult, user) -> partialAgeResult + user.getAge(), Integer::sum);
assertThat(result).isEqualTo(65);
To put it simply, if we use sequential streams and the types of the accumulator arguments and the types of its implementation match, we don’t need to use a combiner.
4. Reduzindo em paralelo
Como aprendemos antes, podemos usarreduce() em fluxos paralelizados.
Quando usamos fluxos paralelizados, devemos ter certeza de quereduce() ou qualquer outra operação agregada executada nos fluxos são:
-
associative: o resultado não é afetado pela ordem dos operandos
-
non-interfering: a operação não afeta a fonte de dados
-
statelessedeterministic: a operação não tem estado e produz a mesma saída para uma determinada entrada
Devemos cumprir todas essas condições para evitar resultados imprevisíveis.
Como esperado, as operações realizadas em fluxos paralelizados, incluindoreduce(),, são executadas em paralelo, tirando proveito das arquiteturas de hardware multi-core.
Por razões óbvias,parallelized streams are much more performant than the sequential counterparts. Mesmo assim, eles podem ser um exagero se as operações aplicadas ao fluxo não forem caras ou o número de elementos no fluxo for pequeno.
Obviamente, fluxos paralelos são o caminho certo a seguir quando precisamos trabalhar com fluxos grandes e executar operações agregadas caras.
Vamos criar um teste de benchmarkJMH (o Java Microbenchmark Harness) simples e comparar os respectivos tempos de execução ao usar a operaçãoreduce() em um fluxo sequencial e paralelizado:
@State(Scope.Thread)
private final List userList = createUsers();
@Benchmark
public Integer executeReduceOnParallelizedStream() {
return this.userList
.parallelStream()
.reduce(
0, (partialAgeResult, user) -> partialAgeResult + user.getAge(), Integer::sum);
}
@Benchmark
public Integer executeReduceOnSequentialStream() {
return this.userList
.stream()
.reduce(
0, (partialAgeResult, user) -> partialAgeResult + user.getAge(), Integer::sum);
}
In the above JMH benchmark, we compare execution average times. Simplesmente criamos umList contendo um grande número de objetosUser. Em seguida, chamamosreduce() em um fluxo sequencial e paralelizado e verificamos se o último executa mais rápido do que o anterior (em segundos por operação).
Estes são os nossos resultados de referência:
Benchmark Mode Cnt Score Error Units
JMHStreamReduceBenchMark.executeReduceOnParallelizedStream avgt 5 0,007 ± 0,001 s/op
JMHStreamReduceBenchMark.executeReduceOnSequentialStream avgt 5 0,010 ± 0,001 s/op
5. Jogando e Lidando com exceções ao reduzir
Nos exemplos acima, a operaçãoreduce() não lança nenhuma exceção. Mas pode, é claro.
Por exemplo, digamos que precisamos dividir todos os elementos de um fluxo por um fator fornecido e depois somar:
List numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
int divider = 2;
int result = numbers.stream().reduce(0, a / divider + b / divider);
Isso funcionará, contanto que a variáveldivider não seja zero. Mas se for zero,reduce() lançará uma exceçãoArithmeticException: dividir por zero.
Podemos facilmente capturar a exceção e fazer algo útil com ela, como registrá-la, recuperá-la e assim por diante, dependendo do caso de uso, usando um blocotry/catch:
public static int divideListElements(List values, int divider) {
return values.stream()
.reduce(0, (a, b) -> {
try {
return a / divider + b / divider;
} catch (ArithmeticException e) {
LOGGER.log(Level.INFO, "Arithmetic Exception: Division by Zero");
}
return 0;
});
}
Embora essa abordagem funcione,we polluted the lambda expression with the try/catch block. Não temos mais a linha única que tínhamos antes.
Para corrigir esse problema, podemos usarhttps://refactoring.com/catalog/extractFunction.html, and extract the try/catch block into a separate method:
private static int divide(int value, int factor) {
int result = 0;
try {
result = value / factor;
} catch (ArithmeticException e) {
LOGGER.log(Level.INFO, "Arithmetic Exception: Division by Zero");
}
return result
}
Agora, a implementação do métododivideListElements() está novamente limpa e simplificada:
public static int divideListElements(List values, int divider) {
return values.stream().reduce(0, (a, b) -> divide(a, divider) + divide(b, divider));
}
Assumindo quedivideListElements() é um método utilitário implementado por uma classe abstrataNumberUtils, podemos criar um teste de unidade para verificar o comportamento do métododivideListElements():
List numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
assertThat(NumberUtils.divideListElements(numbers, 1)).isEqualTo(21);
Vamos também testar o métododivideListElements(), quando os valoresList deInteger fornecidos contêm um 0:
List numbers = Arrays.asList(0, 1, 2, 3, 4, 5, 6);
assertThat(NumberUtils.divideListElements(numbers, 1)).isEqualTo(21);
Por fim, vamos testar a implementação do método quando o divisor for 0 também:
List numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
assertThat(NumberUtils.divideListElements(numbers, 0)).isEqualTo(0);
6. Conclusão
Neste tutorial,we learned how to use the Stream.reduce() operation. In addition, we learned how to perform reductions on sequential and parallelized streams, and how to handle exceptions while reducing.
Como de costume, todos os exemplos de código mostrados neste tutorial estão disponíveisover on GitHub.