domingo, 23 de noviembre de 2014

Reduccion en Colletions (Java 8)

Reduction

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.

Método Stream.reduce

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:

Método Stream.collect

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

No hay comentarios :

Publicar un comentario