La sección de Aggregate Operations explica la siguiente cadena de operaciones, que calcula la edad media de edad de hombres en la colección roster
:
double average = roster
.stream()
.filter(p -> p.getGender() == Person.Sex.MALE)
.mapToInt(Person::getAge)
.average()
.getAsDouble();
El JDK contiene muchas operaciones primitivas (como average
, sum
, min
, max
, y count
) que devuelven un valor generado a partir de combinar el contenido de un stream. Estas operaciones se denominan reduction operations. El JDK contiene además operaciones que devuelven una colección en lugar de un valor simple. Muchas de las operaciones de reducción realizan una tarea concreta, como por ejemplo calcular la media o agrupar los elementos en categorias. Sin embargo, el JDK aporta las operaciones genericas de reduce
y collect
, que explicaremos en detalle..
Esta sección contiene los siguientes temas:
Puedes obtener el código fuente del ejemplo en ReductionExamples
.
Stream.reduce
es un método de propósito general para la operación de reducción. Consideremos la siguiente pipeline, que calcula la suma de hombres en la colección roster
. Se realiza la llamada a la operación de reducción Stream.sum
:
Integer totalAge = roster
.stream()
.mapToInt(Person::getAge)
.sum();
Comparemos esto con la siguiente pipeline, que llama a Stream.reduce
para realizar la mísma operación:
Integer totalAgeReduce = roster
.stream()
.map(Person::getAge)
.reduce(
0,
(a, b) -> a + b);
La operación reduce
en este ejemplo toma dos argumentos:
-
identity
: El elemento identity es al mismo tiempo el valor inicial de la reducción y el valor de retorno por defecto si no existen otros elementos en el stream. En este ejemplo, el elemento identity es 0; este es el valor inicial de la suma de edades y el valor por defecto si no existen otros elementos en la colección roster
.
-
accumulator
: La función accumulator toma dos parámetros: como resultado parcial de la reducción (en este ejemplo, la suma de todos los números procesados hasta el momento) y el siguiente parámetro es el siguiente elemento del stream (en el ejemplo, el siguiente número). El valor de retorno es el resultado parcial. En el ejemplo, la función accumulator es una expresión lambda que suma dos Integer
y devuelve un Integer
:
(a, b) -> a + b
La operación reduce
devuelve siempre un nuevo valor. Sin embargo, la función accumulator retorna tambien un nuevo valor cada vez que se procesa un elemento del stream. Supongamos que queremos reducir los elementos de un stream para obtener un objeto más complejo, como una Collection. Esto puede hinder () el rendimiento de la aplicación. Si se aplica la operación reduce
esto implica añadir los elementos a la colección, y a continuación cada vez que la función accumulator procesa el elemento, se crea una nueva colección que contiene el elemento, lo cual es ineficiente. Una solución más conveniente seria que actualizasemos una colección ya existente. Esto puede realizarse con Stream.collect
, lo que describimos en el siguiente ejemplo:
A diferencia del método reduce
, que crea siempre un nuevo valor cuando procesamos un elemento, el método collect
modifica o transforma un valor existente.
Veamos como calcular la media entre los valores de un stream. Necesitamos dos datos: el número total de valores y la suma de dichos valores. En contraposición a el método reduce
u otro tipo de reducciones, collect
mdevuelve un único valor. Puedes crear un nuevo tipo de dato que contenga variables internas que recojan el número de elementos procesados y la suma de dichos valores, tal como muestra la siguiente clase, Averager
:
class Averager implements IntConsumer
{
private int total = 0;
private int count = 0;
public double average() {
return count > 0 ? ((double) total)/count : 0;
}
public void accept(int i) { total += i; count++; }
public void combine(Averager other) {
total += other.total;
count += other.count;
}
}
La siguiente pipeline incluye la clase Averager
y el método collect
para calcular la media de edad de hombres.
Averager averageCollect = roster.stream()
.filter(p -> p.getGender() == Person.Sex.MALE)
.map(Person::getAge)
.collect(Averager::new, Averager::accept, Averager::combine);
System.out.println("Average age of male members: " +
averageCollect.average());
collect
en este ejemplo toma tres parametros:
supplier
: supplier es una función factory; contruye nuevas instancias. La operación collect
, crea instancias del resultado, en este caso una nueva instacia de la clase Averager
.
accumulator
: Incorpora un elemento de tipo stream dentro de un objeto resultado. En este caso, modifica el resultado Averager
incrementando la variable count
en uno y sumando el valor a la variable total
con el valor del elemento del stream, que es el entero que contiene la edad de un hombre.
combiner
: Toma dos objetos resultado y mezcla sus contenidos. En este ejemplo, modifica un resultado Averager
incrementando su variable count
con el valor de la variable count
del otro miembro Averager
y sumando la variable total
con la otra variable total
del otro miembro Averager
.
Veamos los siguiente :
- Supplier es una expresión lambda (o una referencia a método) en contraposición a lo que vemos en la operación
reduce
con el elemento identity.
- Las funciones accumulator y combiner no devuelven resultado.
- Podemos usar operaciones de tipo
collect
junto con parallel streams; ver la sección Parallelismo para más información. (Si lanzamos un método collect
con un parallel stream, JDK crea un nuevo thread cada vez que la función combiner crea un nuevo objeto, como el objeto Averager
en el example. Como consecuencia, no es nesario que tengamos que ocuparnos de problemas de sincronizacion.)
Aunque el JDK contiene un método average
por defecto para calcular la media de elementos en un stream, podemos usar la operación collect
y una clase propia si lo que queremos es calcular varios valores a partir de los elementos de un stream.
Para el manejo de colecciones es recomendable usar la operación collect
. El ejemplo siguiente añade los nombres de hombres de los objetos persona leidos, dentro de una colección a través de la operación collect
:
List<String> namesOfMaleMembersCollect = roster
.stream()
.filter(p -> p.getGender() == Person.Sex.MALE)
.map(p -> p.getName())
.collect(Collectors.toList());
Esta versión de collect
toma un parámetro de tipo Collector
. Esta clase encapsula las funciones que se usan como argumentos en la operación collect
la cual requiere tres parámetros (supplier, accumulator, y combiner functions).
La clase Collectors
contiene muchas operaciones de reducción, como almacenar (accumulate) dentro de colecciones y resumirlos (procesar un filtrado sobre dichos elementos) según varios criterios. Estas operaciones de reducción devuelven instancias de la clase Collector
, que podemos usar como parámetro de la operación collect
.
En este ejemplo se usa Collectors.toList
, que acumula los elementos del stream dentro de una nueva instacia de List
. Como la mayoria de operaciones de la clase Collectors
, el operador toList
devuelve una instancia de Collector
, y no una colección.
El ejemplo siguiente agrupa los miembre de la colección roster
por género:
Map<Person.Sex, List<Person>> byGender =
roster
.stream()
.collect(
Collectors.groupingBy(Person::getGender));
La operación groupingBy
devuelve un map cuyas claves son los valores que resultan de aplicar la expresión lambda que se pasa como parámetro (llamada classification function). En el ejemplo, el map devuelto contiene dos claves, Person.Sex.MALE
y Person.Sex.FEMALE
. Los valores asociados a las claves son instancias de tipo List
que contienen los elementos del stream que, una vez procesados por la función de clasificación, se asocian al valor de la clave. Por ejemplo, el valor que se corresponde con Person.Sex.MALE
es una instancia de tipo List
que contiene todo los elementos persona de género hombre.
El siguiente ejemplo recupera los nombres de cada miembro de la colección roster
y los agrupa por género:
Map<Person.Sex, List<String>> namesByGender =
roster
.stream()
.collect(
Collectors.groupingBy(
Person::getGender,
Collectors.mapping(
Person::getName,
Collectors.toList())));
La operación groupingBy
en este ejemplo toma dos parámetros, una función de clasificación y una instancia de Collector
. El parámetro Collector
es llamado downstream collector. Esto es un collector que el runtime Java aplica a los resultados de otro collector. En consecuencia, esta operación groupingBy
nos permite aplicar el método collect
a los valores del List
que han sido creados por el operador groupingBy
. Este ejemplo aplica el collector mapping
, que realiza un maping de la función Person::getName
a cada uno de los elementos del stream. De esta manera, el stream resultante contiene únicamente los nombres de los elementos. El pipeline que contiene uno o más collectors del stream de bajada, se denomina un multilevel reduction.
El siguiente ejemplo recupera la edad de los miembros agrupados por género:
Map<Person.Sex, Integer> totalAgeByGender =
roster
.stream()
.collect(
Collectors.groupingBy(
Person::getGender,
Collectors.reducing(
0,
Person::getAge,
Integer::sum)));
La operación de reduccion
toma tres parámetros:
identity
: Como la operación Stream.reduce
, el elemento identity es el elemento inicial de la reducción y el resultado si no existen elementos en el stream. En el ejemplo, el elemento identity es 0; este es el valor inicial de la suma de edades y el valor por defecto si no existen miembros.
mapper
: La operación de reduccion
aplica la función de mapeo a todos los elementos del stream. En el ejemplo, el mapper recupera la edad de cada elemento.
operation
: La función operation se usa para reducir los valores mapeados. En el ejemplo, la función de operación suma valores Integer
.
El ejemplo siguiente recupera la media de edad de los elementos de cada uno de los géneros:
Map<Person.Sex, Double> averageAgeByGender = roster
.stream()
.collect(
Collectors.groupingBy(
Person::getGender,
Collectors.averagingInt(Person::getAge)));