Una reducción es una operación que toma muchos elementos y los combina para reducirlos a un solo valor u objeto. La reducción se hace aplicando una operación varias veces.
Algunos ejemplos de reducciones son la suma de N elementos, el hallazgo del elemento máximo de N números o el conteo de elementos.
En el siguiente ejemplo, usamos un bucle for para reducir un conjunto de números a su suma:
12345int[] números ={1,2,3,4,5,6};int suma =0;for(int n : números){ suma += n;}
java
Por supuesto, hacer las reducciones con corrientes en lugar de bucles tiene beneficios, como una paralelización más fácil y una mejor legibilidad.
La interfaz de la corriente tiene dos métodos para la reducción:
12collect()reduce()
java
Podemos implementar reducciones con ambos métodos, pero collect() nos ayuda a implementar un tipo de reducción llamada reducción mutable , donde se utiliza un contenedor (como una Collection) para acumular el resultado de la operación.
La otra operación de reducción, reduce(), tiene tres versiones:
12345678Opcional<T,T;reducir(BinaryOperator<T,acumulador)/ Ureduce(T identidad,BinaryOperator<T,acumulador)/ Ureduce(U identidad,BiFunction<U,?superT,U,;acumulador,BinaryOperator<U,;combinador)
java
Recuerde que un Operador Binario es equivalente a una Bifunción, donde los dos argumentos y el tipo de retorno son todos del mismo tipo.
Empecemos con la versión que toma un argumento. Esto es equivalente a:
1234567891011elementos booleanosFound =falso;T resultado =nulo;for(T element : stream){if(!elementsFound){ elementsFound =true; result = element;}else{ resultado = accumulator.apply(result, element);}retorno elementosFound ?Opcional.of(result):Opcional.empty();
java
Este código sólo aplica una función para cada elemento, acumulando el resultado y devolviendo una envoltura Opcional ese resultado, o una Opcional vacía si no hubiera elementos.
Veamos un ejemplo concreto. Vemos cómo una suma es una operación de reducción:
12345int[] números ={1,2,3,4,5,6};int suma =0;for(int n : números){ suma += n;}
java
Aquí, el funcionamiento del acumulador es:
1sum += n;//o suma = suma + n
java
Esto se traduce en:
12OpcionalInt total =IntStream.of(1,2,3,4,5,6).reduce((sum, n)- > sum + n );
java
Fíjate en cómo la versión primitiva de Stream utiliza la versión primitiva de Opcional.
Esto es lo que pasa paso a paso:
- Una variable interna que acumula el resultado se fija en el primer elemento de una corriente (1).
- Este acumulador y el segundo elemento de la corriente (2) se pasan como argumentos al Operador Binario representado por la expresión lambda (suma, n) -------; suma + x.
- El resultado (3) se asigna al acumulador.
- El acumulador (3) y el tercer elemento de la corriente (3) se pasan como argumentos al Operador Binario.
- El resultado (6) se asigna al acumulador.
- Los pasos 4 y 5 se repiten para los siguientes elementos de la corriente hasta que no haya más elementos.
Sin embargo, ¿qué pasa si necesitas tener un valor inicial? Para casos como ese, tenemos la versión que requiere dos argumentos:
1Treduce(T identidad,BinarioOperador<T;acumulador)
java
El primer argumento es el valor inicial, y se llama la identidad porque, en sentido estricto, este valor debe ser una identidad para la función de acumulador. Es decir, para cada valor v, acumulador.apply(identidad, v) debe ser igual a v.
Esta versión de reducir() es equivalente a:
12345T result = identity;for(T element : stream){ result = accumulator.apply(result, element);}return result;
java
Fíjese que esta versión no devuelve un objeto opcional porque si la corriente se vacía, se devuelve el valor de identidad.
Por ejemplo, el ejemplo de la suma puede ser reescrito como:
123int total =IntStream.of(1,2,3,4,5,6).reduce(0,(sum, n)-> sum + n );// 21
java
O usando un valor inicial diferente:
123int total =IntStream.of(1,2,3,4,5,6).reduce(4,(sum, n)- > sum + n );// 25
java
Sin embargo, nótese que en el ejemplo anterior, el primer valor no puede considerarse una identidad porque, por ejemplo, 4 + 1 no es igual a 4.
Esto puede traer algunos problemas cuando se trabaja con corrientes paralelas, que revisaremos en unos momentos.
Ahora, noten que con estas versiones, se toman elementos de tipo T y se devuelve un valor reducido de tipo T también.
Sin embargo, si quieres devolver un valor reducido de un tipo diferente, tienes que usar la versión de tres argumentos de reduce():
123<U>Ureduce(U identidad,BiFunción<U,?superT,U> acumulador,BinarioOperador<U,combinador)
java
Esto equivale a usar:
12345U resultado = identidad;for(elemento T : corriente){ resultado = acumulador.apply(resultado, elemento)}retorno resultado;
java
Consideremos por ejemplo que queremos obtener la suma de la longitud de todas las cuerdas de un arroyo, por lo que tomamos las cuerdas (tipo T), y queremos un resultado entero (tipo U).
En ese caso, usamos reducir() así:
1234567int longitud =Stream.of("Paralelo", "corrientes", "son", "gran").reduce(0,(accumInt, str)- > accumInt + str.longitud(),//acumulador(accumInt1, accumInt2)- ]; accumInt1 + accumInt2);//combinador
java
Podemos hacerlo más claro añadiendo los tipos de argumentos:
1234567int length =Stream.of("Parallel", "streams", "are", "great").reduce(0,(Integer accumInt,String str)- > accumInt + str.length(),//acumulador(Integer accumInt1,Integer accumInt2)- > accumInt1 + accumInt2);//combinador
java
Como la función de acumulador añade un paso de mapeo (transformación) a la función de acumulador, esta versión del método reduce() puede escribirse como una combinación de map() y las otras versiones del método reduce() (puede conocerse como el patrón de mapeo-reducción):
123456int longitud =Stream.of("Paralelo", "corrientes", "son", "gran").mapToInt(s - > s.longitud()).reduce(0,(suma, strLength)-> suma + strLength);
java
O simplemente:
123int length =Stream.of("Parallel", "streams", "are", "great").mapToInt(s - > s.length()).sum();
java
De hecho, las operaciones de cálculo que aprendimos en la primera parte se implementan como operaciones de reducción bajo el capó:
12345averagecountmaxminsum
Además, observe que si devolvemos un valor del mismo tipo, la función combinadora ya no es necesaria (resulta que esta función es la misma que la del acumulador). Así que, en este caso, es mejor usar la versión de dos argumentos.
Se recomienda usar el método de las tres versiones de reducir() cuando:
- Trabajando con corrientes paralelas
- El hecho de tener una sola función como mapeador y acumulador es más eficiente que tener funciones separadas de mapeo y reducción.
Por otro lado, collect() tiene dos versiones:
12345<R,A>Rcollect(Colector<?superT,A,R,R,coleccionista)<;R,Rcollect(Proveedor<R,R,proveedor,BiConsumidor<R,?superT,;acumulador,BiConsumidor<R,R,R,combinador)
java
La primera versión utiliza coleccionistas predefinidos de la clase Coleccionistas mientras que la segunda permite crear sus propios coleccionistas. Los flujos primitivos (como el IntStream) sólo tienen esta última versión de collect().
Recuerde que collect() realiza una reducción mutable en los elementos de una corriente, lo que significa que utiliza un objeto mutable para acumular, como una Collection o un StringBuilder. Por el contrario, reduce() combina dos elementos para producir uno nuevo y representa una reducción inmutable.
Sin embargo, comencemos con la versión que toma tres argumentos, ya que es similar a la versión reducida() que también toma tres argumentos.
Como se puede ver en su firma, primero se toma un Proveedor que devuelve el objeto que será utilizado y devuelto como un contenedor (acumulador).
El segundo parámetro es una función de acumulador, que toma el contenedor y el elemento que se añade a él.
El tercer parámetro es la función combinadora, que fusiona los resultados intermedios en el final (útil cuando se trabaja con corrientes paralelas).
Esta versión de collect() es equivalente a:
12345R result = supplier.get();for(T element : stream){ accumulator.accept(result, element);}return result;
java
Por ejemplo, si queremos "reducir" o "reunir" todos los elementos de una corriente en una Lista, utilice el siguiente algoritmo:
1234567List<Integer;a; list =Stream.of(1,2,3,4,5).collect(()-(()-;newArrayList<m;(),// Creando el contenedor(l, i)-(); l.add(i),// Añadiendo un elemento(l1, l2)-(); l1.addAll(l2)// Combinando elementos);
java
Podemos hacerlo más claro añadiendo los tipos de argumentos:
java
También podemos utilizar referencias de métodos:
1234567List<Integer;a; list =Stream.of(1,2,3,4,5).collect(ArrayList::new,ArrayList::add,ArrayList::addAll);
java