Guía para recopiladores de Java 8: coleccionar y luego ()

En esta guía definitiva y detallada, aprenda todo lo que necesita saber sobre la recopilación y el análisis de datos de Java Collectors, si es eficiente y pruebe un proyecto práctico de análisis de datos.

Introducción

Una secuencia representa una secuencia de elementos y admite diferentes tipos de operaciones que conducen al resultado deseado. La fuente de un flujo suele ser una Colección o un Array, desde el cual se transmiten los datos.

Los flujos se diferencian de las colecciones en varios aspectos; sobre todo porque los flujos no son una estructura de datos que almacena elementos. Son de naturaleza funcional, y vale la pena señalar que las operaciones en un flujo producen un resultado y, por lo general, devuelven otro flujo, pero no modifican su origen.

Para "solidificar" los cambios, reúne los elementos de un flujo en una Colección.

Los recopiladores representan implementaciones de la interfaz Collector, que implementa varias operaciones de reducción útiles, como acumular elementos en colecciones, resumir elementos en función de un parámetro específico, etc.

Todas las implementaciones predefinidas se pueden encontrar dentro de la clase Collectors.

Sin embargo, también puede implementar muy fácilmente su propio recopilador y usarlo en lugar de los predefinidos; puede llegar bastante lejos con los recopiladores integrados, ya que cubren la gran mayoría de los casos en los que es posible que desee usarlos.

Para poder usar la clase en nuestro código necesitamos importarla:

1
import static java.util.stream.Collectors.*;

Stream.collect() realiza una operación de reducción mutable en los elementos de la secuencia.

{.icon aria-hidden=“true”}

Una operación de reducción mutable recopila elementos de entrada en un contenedor mutable, como una Colección, mientras procesa los elementos de la secuencia.

En esta guía, profundizaremos en el recopilador collectingAndThen().

¿Qué hace colectingAndThen()?

La operación collectingAndThen() acepta dos parámetros:

1
collectingAndThen(Collector d, Function f);

Primero llama a un colector preexistente, d y realiza una función final, f sobre el resultado de d.

Echemos un vistazo rápido a cómo podríamos usar el método collectingAndThen() en un flujo de enteros:

1
Stream<Integer> s = Stream.of(12, 13, 14, 15)

Ahora, suponga que desea recopilar estos valores en una lista no modificable de objetos Integer. Como primer intento, crearíamos una lista de los valores Integer:

1
2
3
4
5
6
7
8
9
List<Integer> list = Stream.of(12, 13, 14, 15)
    .collect(
    //Supplier
    () -> new ArrayList<Integer>(),
    //Accumulator
    (l, e) -> l.add(e),
    //Combiner
    (l, ar) -> l.addAll(ar)
);        

Hemos recopilado los elementos de la transmisión en una lista usando tres parámetros:

  • Proveedor

  • Acumulador

  • Combinador

Aún así, para un paso tan simple, esto es un poco demasiado detallado. Afortunadamente, tenemos el método toList() en la clase auxiliar Collectors. Por lo tanto, podríamos simplificar el paso escribiendo:

1
list = Stream.of(12, 13, 14, 15).collect(toList());

De acuerdo, hemos compactado el código en una sola línea. Sin embargo, cuando verificamos la clase de la lista que hemos producido:

1
System.out.println(list.getClass().getSimpleName());

Esto resulta en:

1
ArrayList

Queríamos una lista no modificable. Y ArrayList no es uno. Una solución sencilla sería llamar al método unmodifiableList() desde Collections:

1
List<Integer> ul = Collections.unmodifiableList(list);

Y al comprobar qué clase tenemos como resultado:

1
System.out.println(ul.getClass().getSimpleName());

Obtenemos la salida:

1
UnmodifiableRandomAccessList

Oye, pero ¿qué es una Lista de acceso aleatorio no modificable? Cuando verifique el código fuente de JDK, verá que extiende UnmodifiableList

Por lo que UnmodifiableList:

Devuelve una vista no modificable de la lista especificada. Esta [clase] permite que los módulos proporcionen a los usuarios acceso de "solo lectura" a las listas internas

Hasta ahora, parece que hemos cumplido nuestro objetivo de crear una lista no modificable a partir de un flujo de valores int, pero hemos tenido que trabajar mucho para lograrlo.

Este es el escenario exacto que Java intenta remediar con collectingAndThen().

Lo que queremos hacer es recolectar los enteros, y luego hacer otra cosa (convertir la lista en una que no se pueda modificar), que es exactamente lo que podemos hacer con recolectarYEntonces():

1
2
3
4
5
6
7
ul = Stream.of(12, 13, 14, 15)
    .collect(
    Collectors.collectingAndThen(
        Collectors.toList(),
        Collections::unmodifiableList
    )
);

Y, nuestro resultado, ul, es del tipo: UnmodifiableList. ¡La navaja de Occam ataca de nuevo! Sin embargo, hay mucho más que decir sobre el método.

¿Cómo funciona realmente? ¿Es eficiente? ¿Cuándo deberías usarlo? ¿Cómo lo ponemos en práctica?

Esta guía tiene como objetivo responder a todas estas preguntas.

Definición de recogerYDespués()

Firma del método

El método collectingAndThen() es un método de fábrica en la clase auxiliar - Collectors, una parte de Stream API:

1
2
3
4
public static <T, A, R, RR> Collector<T, A, RR> collectingAndThen(
    Collector<T, A, R> downstream, 
    Function<R, RR> finisher
) {...}

Donde los parámetros representan:

  • downstream: el colector inicial al que llamará la clase Collectors.
  • finisher: la función que la clase Collectors aplicará en downstream.

Y, los tipos genéricos representan:

  • T: tipo de clase de los elementos del stream.
  • A: tipo de clase de los elementos después del paso de acumulación del colector aguas abajo.
  • R: tipo de clase de los elementos después de que downstream termine de recopilar.
  • RR: tipo de clase de los elementos después de aplicar finisher en downstream.

Y, el valor de retorno es:

  • Collector<T, A, RR>: un colector que resulta de la aplicación de finisher en downstream.

Descripción

El javadoc oficial establece que el método colectingAndThen() es útil porque:

Adapta un Collector para realizar una transformación de acabado adicional.

No hay mucho que agregar a esto: a menudo realizamos acciones en las colecciones después de recopilarlas, ¡y esto lo hace mucho más fácil y menos detallado!

¿Cómo funciona recogerYDespués()?

El siguiente diagrama de actividad UML resume el flujo de control en una operación collectingAndThen(). Es una abstracción de alto nivel de lo que siempre podría ocurrir en una operación de este tipo; sin embargo, muestra cómo funcionan las rutinas en los pasos de transmisión, recopilación y finalización:

 Activity Diagram

¿Cuándo debe usar colectingAndThen()?

1. Cuando necesitamos un tipo de objeto diferente al que ofrece una única operación collect():

1
2
3
4
5
6
7
8
List<Integer> list = Arrays.asList(1, 2, 3);

Boolean empty = list.stream()
    .collect(collectingAndThen(
        toList(),
        List::isEmpty
    )
);

Aquí, logramos obtener un Booleano de la Lista que collect() habría devuelto.

2. Cuando necesitamos posponer el procesamiento hasta que podamos encontrar todos los elementos en un flujo dado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
String longestName = people.stream()
    .collect(collectingAndThen(
        // Encounter all the Person objects 
        // Map them to their first names
        // Collect those names in a list
        mapping(
            Person::getFirstName,
            toList()
        ),
        // Stream those names again
        // Find the longest name
        // If not available, return "?"
        l -> {
            return l
                .stream()
                .collect(maxBy(
                    comparing(String::length)
                ))
                .orElse("?");
        }
    )
);

Aquí, por ejemplo, solo calculamos la cadena más larga después de leer todos los nombres de ‘Personas’.

3. Y, cuando necesitamos ajustar una lista para que no se pueda modificar:

1
2
3
4
5
6
7
List<Integer> ul = Stream.of(12, 13, 14, 15)
    .collect(
    Collectors.collectingAndThen(
        Collectors.toList(),
        Collections::unmodifiableList
    )
);

¿RecogerYDespués() es eficiente?

En algunos casos de uso, puede reemplazar una operación collectingAndThen() sin cambiar el resultado de su método. Por lo tanto, surge la pregunta: ¿el uso de collectingAndThen() ofrecería tiempos de ejecución más rápidos?

Por ejemplo, suponga que tiene una colección de nombres y desea saber cuál de ellos es el más largo. Vamos a crear una clase Persona, que contendría el nombre completo de alguien: primero y último:

1
2
3
4
5
6
public class Person {
    private final String first;
    private final String last;
    
    // Constructor, getters and setters
}

Y digamos que tienes un ExecutionPlan que genera bastantes objetos Person:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@State(Scope.Benchmark)
public class ExecutionPlan {
    private List<Person> people;
    
    @Param({"10", "100", "1000", "10000", "100000"})
    int count;
    
    @Setup(Level.Iteration)
    public void setup() {
        people = new ArrayList<>();        
        Name fakeName = new Faker().name();
        
        for (int i = 0; i < count; i++) {
            String fName = fakeName.firstName();
            String lName = fakeName.lastName();
            Person person = new Person(fName, lName);
            
            people.add(person);
        }
    }
    
    public List<Person> getPeople() {
        return people;
    }
}

{.icon aria-hidden=“true”}

Nota: Para generar fácilmente muchos objetos falsos con nombres sensatos, usamos la biblioteca Falsificador de Java. También puedes incluirlo en tus Proyectos expertos.

La clase ExecutionPlan dicta el número de objetos Person que puedes probar. Usando un arnés de prueba (JMH), el campo count haría que el bucle for en setup() emitiera tantos objetos Person.

Encontraremos el primer nombre más largo usando dos enfoques:

  1. Usando la operación intermedia de Stream API, sort().
  2. Usando recolectandoYLuego().

El primer enfoque utiliza el método withCollectingAndThen():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public void withoutCollectingAndThen() {
    Comparator nameLength = Comparator.comparing(String::length)
        .reversed();
    
    String longestName = people
        .stream()
        .map(Person::getFirstName)
        .sorted(nameLength)
        .findFirst()
        .orElse("?")
}

Este enfoque asigna un flujo de objetos Person a sus nombres. Luego, ordena la longitud de los nombres en orden descendente. Utiliza el método estático comparing() de la interfaz Comparator. Debido a que comparing() haría que la ordenación se listara en orden ascendente, llamamos reversed(). Esto hará que la secuencia contenga valores que comiencen con el más grande y terminen con el más pequeño.

Concluimos la operación llamando a findFirst(), que selecciona el primer valor más grande. Además, debido a que el resultado será un ‘Opcional’, lo transformamos en una ‘Cadena’ con ‘orElse()’.

El segundo enfoque utiliza el método withCollectingAndThen():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public void withCollectingAndThen() {    
    Collector collector = collectingAndThen(
        Collectors.maxBy(Comparator.comparing(String::length)),
        s -> s.orElse("?")
    );
    
    String longestName = people.stream()
        .map(Person::getFirstName)
        .collect(collector);        
}

Este enfoque es más conciso porque contiene el colector descendente, maxBy(), por lo que no tenemos que ordenar, invertir y encontrar el primer elemento. Este método es uno de los muchos métodos estáticos de la clase Collectors. Es conveniente de usar porque devuelve un solo elemento de una secuencia: el elemento con el valor más grande. Lo único que nos queda es proporcionar una implementación Comparator para ayudar a calcular este valor.

En nuestro caso, estamos buscando la Cadena con la longitud más larga, por lo que usamos Comparator.comparing(String::length). Aquí también, tenemos que lidiar con un ‘Opcional’. La operación maxBy() produce uno, que luego convertimos en un simple String en el paso del finalizador.

Si comparamos estos dos métodos en 10, 100, 1000, 10000 y 100000 instancias de ‘Personas’ usando JMH, obtenemos un resultado bastante claro:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Benchmark                                            (count)   Mode  Cnt        Score   Error  Units
CollectingAndThenBenchmark.withCollectingAndThen          10  thrpt    2  7078262.227          ops/s
CollectingAndThenBenchmark.withCollectingAndThen         100  thrpt    2  1004389.120          ops/s
CollectingAndThenBenchmark.withCollectingAndThen        1000  thrpt    2    85195.997          ops/s
CollectingAndThenBenchmark.withCollectingAndThen       10000  thrpt    2     6677.598          ops/s
CollectingAndThenBenchmark.withCollectingAndThen      100000  thrpt    2      317.106          ops/s
CollectingAndThenBenchmark.withoutCollectingAndThen       10  thrpt    2  4131641.252          ops/s
CollectingAndThenBenchmark.withoutCollectingAndThen      100  thrpt    2   294579.356          ops/s
CollectingAndThenBenchmark.withoutCollectingAndThen     1000  thrpt    2    12728.669          ops/s
CollectingAndThenBenchmark.withoutCollectingAndThen    10000  thrpt    2     1093.244          ops/s
CollectingAndThenBenchmark.withoutCollectingAndThen   100000  thrpt    2       94.732          ops/s

{.icon aria-hidden=“true”}

Nota: JMH asigna una puntuación en lugar de medir el tiempo que lleva ejecutar una operación de referencia. Las unidades utilizadas fueron operaciones por segundo, por lo que cuanto mayor sea el número, mejor, ya que indica un mayor rendimiento.

Cuando pruebas con diez objetos Person, collectingAndThen() se ejecuta el doble de rápido que sort(). Mientras que collectingAndThen() puede ejecutar 7,078,262 operaciones en un segundo, sort() ejecuta 4,131,641.

Pero, con diez mil de esos objetos, collectingAndThen() muestra resultados aún más impresionantes. ¡Se ejecuta seis veces más rápido que sort()! En conjuntos de datos más grandes, muy claramente supera a la primera opción, por lo que si está tratando con muchos registros, obtendrá importantes beneficios de rendimiento de collectingAndThen().

{.icon aria-hidden=“true”}

Encuentre el informe completo de los resultados de la prueba en GitHub. Todo el arnés de prueba también está en este [repositorio GitHub] (https://github.com/IdelsTak/wikihtp-tutorials/tree/master/collectingAndThen/src/main/java/com/github/idelstak/collectingandthen/benchmark). Adelante, clónelo y ejecútelo en su máquina local y compare los resultados.

Poner en práctica recopilación y luego(): análisis del conjunto de datos de contaminación en interiores {#recopilación y práctica del análisis de datos de contaminación en interiores}

Hasta ahora, hemos visto que collectingAndThen() puede adaptar un recopilador con un paso adicional. Sin embargo, esta capacidad es aún más poderosa de lo que piensas. Puede anidar collectingAndThen() dentro de otras operaciones que también devuelvan instancias de Collector. Y recuerda, collectingAndThen() también devuelve un Collector. Entonces, también puede anidar estas otras operaciones en él:

1
2
3
4
5
6
7
8
9
stream.collect(groupingBy(
        groupingBy(
            collectingAndThen(
                downstream,
                finisher
            )
        )
    )    
);

Esta posibilidad abre un montón de opciones de diseño de código. Puede, por ejemplo, usarlo para agrupar los elementos de un flujo. O, dividirlos de acuerdo con un ‘Predicado’ dado.

If you'd like to read more about Predicates - read our Programación Funcional en Java 8: Guía Definitiva de Predicados!

Veremos cómo funciona esto usando datos de las muertes que causa la contaminación del aire interior. Estos datos contienen las tasas de mortalidad por cada 100.000 habitantes. Nuestro mundo en datos (OWID) lo ha categorizado por edad y por año. Contiene hallazgos de la mayoría de los países y regiones del mundo. Además, cubre los años de 1990 a 2017.

Diseño de dominio {#diseño de dominio}

El dominio contiene tres clases principales: Mortalidad, CountryStats y StatsSource. La clase Mortalidad contiene dos campos: Grupo de edad y mortalidad. En esencia, la clase Mortalidad es una clase de valor.

Mira, tenemos la opción de tratar con los valores de Grupo de edad y mortalidad por sí solos. Sin embargo, eso solo abarrotará el código del cliente. Los valores String que representan grupos de edad no tendrían sentido cuando los usa solos. Lo mismo se aplica a los valores BigDecimal que representan las cifras de mortalidad.

Pero, cuando usa estos dos juntos, aclaran de qué se trata su dominio:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class Mortality implements Comparable {
    private final String ageGroup;
    private final BigDecimal mortality;
    
    //Constructor and getters...
    
    @Override
    public int compareTo(Mortality other) {
        return Comparator.comparing(Mortality::getMortality)
            .compare(this, other);
    }
}

Esta clase también implementa la interfaz Comparable. Esto es importante porque nos ayudaría a clasificar los objetos de Mortalidad. La siguiente clase, CountryStats contiene datos de mortalidad para diferentes grupos de edad. Es otra clase de valor y contiene el nombre de un país/región. Y, el año en el que ocurrieron varias muertes en varios grupos de edad. Por lo tanto, ofrece una instantánea de la historia de las tasas de mortalidad de un país:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class CountryStats {
    private final String country;
    private final String code;
    private final String year;
    private final Mortality underFive;
    private final Mortality seventyPlus;
    private final Mortality fiftyToSixtyNine;
    private final Mortality fiveToFourteen;
    private final Mortality fifteenToFourtyNine;
    
    //Constructor and getters...
    
    public Mortality getHighest() {
        Stream<Mortality> stream = Stream.of(
            underFive,
            fiveToFourteen,
            fifteenToFourtyNine,
            fiftyToSixtyNine,
            seventyPlus
        );
        
        Mortality highest = stream.collect(
            collectingAndThen(
                Collectors.maxBy(
                    Comparator.comparing(
                        Mortality::getMortality
                    )
                ),
                m -> m.orElseThrow(
                    RuntimeException::new
                )
            )
        );
        
        return highest;
    }
}

Su método getHighest() nos ayuda a saber qué grupo de edad tiene la tasa de mortalidad más alta. Utiliza el colector de maxBy() para conocer el objeto Mortalidad con la tasa más alta. Pero, devuelve un Opcional. Por lo tanto, tenemos un paso de acabado adicional que desenvuelve el ‘Opcional’. Y lo hace de una manera que puede lanzar una RuntimeException si Optional está vacío.

La última clase, StatsSource maneja el mapeo de los datos CSV a CountryStats. En el fondo, actúa como una clase de ayuda, que da acceso al archivo CSV que contiene las tasas de mortalidad. Utiliza la biblioteca Apache Commons CSV para leer el archivo CSV que contiene los datos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class StatsSource {
    private List<CountryStats> stats;
    
    public List<CountryStats> getStats() {
        if (stats == null) {
            File f; //Get CSV file containing data
            Reader in = new FileReader(f);
            CSVFormat csvf = CSVFormat
                .DEFAULT
                .builder()
                .setHeader()
                .setSkipHeaderRecord(true)
                .build();
            
            Spliterator split = csvf.parse(in)
                .splitIterator();
            
            stats = StreamSupport
                // Set `true` to make stream parallel
                // Set `false` to make sequential
                .stream(split, false)
                .map(StatsSource::toStats)
                .collect(toList());                
        }
        
        return stats;
    }
    
    public static CountryStats toStats(CSVRecord r) {
        // Constructor...
    }
}

Observe cómo asigna las líneas en el archivo a los objetos CountryStats usando una secuencia. Teníamos la opción de usar StreamSupport para crear un flujo paralelo de líneas usando un indicador true. Pero, optamos por tener una transmisión en serie en lugar de pasar falso a StreamSupport.

Los datos en el archivo CSV vienen en orden alfabético desde la fuente. Sin embargo, al usar un flujo paralelo, perderíamos ese orden.

Usar recopilar y luego () en agrupar {#usar recopilar y luego agrupar}

Queremos presentar los datos de la fuente de varias maneras útiles. Queremos mostrar, por ejemplo, datos pertinentes en categorías de año, país y tasa de mortalidad. Un caso de uso simple sería presentar los datos con solo dos encabezados. Un país y el año que sufrió las mayores tasas de mortalidad de niños menores de cinco años. En otros términos, esto es agrupación de un solo nivel.

En un formato tabulado, por ejemplo, desearíamos lograr esto:


País Año con mayor mortalidad en menores de 5 años Afganistán 1997 Albania 1991 Nigeria 2000 Islas Salomón 2002 Zimbabue 2011


Una más compleja sería listar los países por los años en que ocurrió la mortalidad. Y en esos años, quisiéramos enumerar el grupo de edad que sufrió la mayor mortalidad. En términos estadísticos, nuestro objetivo es la agrupación de datos en varios niveles. En términos simples, la agrupación de varios niveles es similar a la creación de muchos grupos de un solo nivel. Por lo tanto, podríamos representar estas estadísticas como:

Afganistán


Año Grupo de edad con mayor mortalidad 1990 Menores de 5 años 1991 Entre 50 y 69 años 2000 Más de 70 años 2001 Más de 70 años 2010 Menores de 5 años


Papúa Nueva Guinea


Año Grupo de edad con mayor mortalidad 1990 Más de 70 años 1991 Más de 70 años 2000 Entre 5 y 14 años 2001 Entre 5 y 14 años 2010 Entre 15 y 49 años


Y así sucesivamente… para todos los países, desde el año 1990 hasta el 2017.

Agrupación de un solo nivel con recopilación y luego() {#agrupación de un solo nivel con recopilación y luego}

En términos de programación declarativa, tenemos tres tareas que necesitamos que realice el código:

  1. Agrupar los datos de mortalidad por países.
  2. Para cada país, encuentre su tasa de mortalidad más alta para niños menores de cinco años.
  3. Reporte el año en que ocurrió esa tasa alta.
Agrupar por país

Vale la pena considerar una cosa. El archivo CSV con el que estamos tratando enumera los datos de mortalidad de cada país varias veces. Enumera 28 entradas para cada país. Así podríamos crear un ‘Mapa’ a partir de estas entradas. La clave sería el nombre del país y el valor el valor CountryStats. Y esto es exactamente lo que hace el método shouldGroupByCountry():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
private final StatsSource src = new StatsSource();
private List<CountryStats> stats = src.getStats();
private final Supplier exc = RuntimeException::new;

@Test
public void shouldGroupByCountry() {
    Map result = stats.stream().collect(
        Collectors.groupingBy(
            CountryStats::getCountry,
            Collectors.toList()
        )
    );
    
    System.out.println(result);
}

If you'd like to read more about groupingBy() read our Guía de recopiladores de Java 8: groupingBy()!

Este ‘mapa’ es grande, por lo que simplemente imprimirlo en la consola lo haría absolutamente ilegible. En su lugar, podemos formatear la salida insertando este bloque de código justo después de calcular la variable resultado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
result.entrySet()
    .stream()
    .sorted(comparing(Entry::getKey))
    .limit(2)
    .forEach(entry -> {
     entry.getValue()
         .stream()
         .sorted(comparing(CountryStats::getYear))
         .forEach(stat -> {
             System.out.printf(
                 "%s, %s: %.3f\n",
                 entry.getKey(),
                 stat.getYear(),
                 stat.getUnderFive().getMortality()
             );
         });
    });

El valor resultado es del tipo Map<String, List<CountryStats>>. Para que sea más fácil de interpretar:

  • Ordenamos las claves en orden alfabético.
  • Le indicamos a la transmisión que limite su longitud a solo dos elementos Map.
  • Nos ocupamos de generar los detalles de cada elemento usando forEach().
    • We sort the value (a list of CountryStats values) from the key by year.
    • Then, we print the year and its mortality rate for children under five years.

Con eso hecho, ahora podemos obtener una salida como esta:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Afghanistan, 1990: 9301.998
Afghanistan, 1991: 9008.646
# ...
Afghanistan, 2016: 6563.177
Afghanistan, 2017: 6460.592
Albania, 1990: 390.996
Albania, 1991: 408.096
# ...
Albania, 2016: 9.087
Albania, 2017: 8.545
Encontrar la tasa de mortalidad más alta para niños menores de 5 años

Llevamos listando la mortalidad de niños menores de cinco años para todos los años pertinentes. Pero, lo estamos llevando un nivel más alto al seleccionar ese año que tuvo la mortalidad más alta.

Al igual que collectingAndThen(), groupingBy() también acepta un parámetro de finalización. Pero, a diferencia de collectingAndThen(), toma un tipo Collector. Recuerda, collectingAndThen() toma una función.

Trabajando con lo que tenemos entonces, pasamos un maxBy() a groupingBy(). Esto tiene el efecto de crear un Mapa de tipo: Map<String, Optional<CountryStats>>. Es un paso en la dirección correcta porque ahora estamos tratando con un objeto ‘Opcional’ que envuelve un objeto ‘CountryStats’:

1
2
3
4
5
6
result = stats.stream().collect(
    Collectors.groupingBy(
        CountryStats::getCountry,
        Collectors.maxBy(comparing::getUnderFive)
    )
);

Aún así, este enfoque no produce el resultado exacto que buscamos. Nuevamente, tenemos que formatear la salida:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
result.entrySet()
    .stream()
    .sorted(comparing(Entry::getKey))
    .limit(2)
    .forEach(entry -> {
        CountryStats stats = entry
            .getValue()
            .orElseThrow(exc);
        
        System.out.printf(
            "%s, %s: %.3f\n",
            entry.getKey(),
            stat.getYear(),
            stat.getUnderFive().getMortality()
        );
    });

Para que podamos obtener esta salida:

1
2
Afghanistan, 1997: 14644.286
Albania, 1991: 408.096

Por supuesto, la salida cita las cifras correctas que buscábamos. Pero, debería haber otra forma de producir tal salida. Y es bastante cierto, como veremos a continuación, de esa manera implica usar collectingAndThen().

Citar el año con la tasa de mortalidad más alta para niños menores de 5 años {#citar el año con la tasa de mortalidad más alta para niños menores de 5 años}

Nuestro principal problema con el intento anterior es que devolvió un ‘Opcional’ como el valor del elemento ‘Mapa’. Y este ‘Opcional’ envolvió un objeto ‘CountryStats’, que en sí mismo es una exageración. Necesitamos los elementos Map para tener el nombre del país como clave. Y el año como el valor de ese ‘Mapa’.

Entonces, lo lograremos creando el resultado Map con este código:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
result = stats.stream().collect(
    groupingBy(
        CountryStats::getCountry,
        TreeMap::new,
        Collectors.collectingAndThen(
            Collectors.maxBy(
                Comparator.comparing(
                    CountryStats::getUnderFive
                )
            ),
            stat -> {
                return stat
                    .orElseThrow(exc)
                    .getYear();
            }
        )
    )
);

¡Hemos cambiado el intento anterior de tres maneras! Primero, hemos incluido una fábrica Map (TreeMap::new) en la llamada al método groupingBy(). Esto haría que groupingBy() ordenara los nombres de los países en orden alfabético. Recuerde, en los intentos anteriores hicimos llamadas sort() para lograr lo mismo.

Sin embargo, esta es una mala práctica. Forzamos un encuentro de todos los elementos de la corriente incluso antes de aplicar una operación de terminal. Y eso supera toda la lógica de procesar elementos de flujo de manera perezosa.

{.icon aria-hidden=“true”}

La operación sort() es una operación intermedia con estado. Anularía cualquier ganancia que obtendríamos si usáramos una transmisión paralela, por ejemplo.

En segundo lugar, hemos hecho posible obtener un paso adicional del resultado del recopilador maxBy(). Hemos incluido collectingAndThen() para lograrlo. En tercer lugar, en el paso final, hemos transformado el resultado Opcional de maxBy() en un valor de año.

Y bastante cierto, al imprimir el resultado en la consola, esto es lo que obtenemos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
Afghanistan=1997,
Albania=1991,
Algeria=1990,
American Samoa=1990,
Andean Latin America=1990,
Andorra=1990, Angola=1995,
Antigua and Barbuda=1990,
Argentina=1991,
...,
Zambia=1991,
Zimbabwe=2011
}
Agrupación multinivel con recopilación y luego() {#agrupación multinivel con recopilación y luego}

Se podría decir que la tarea anterior se centró en crear datos que puedan caber en una tabla. Uno que tiene dos columnas: un país y un año con la mayor mortalidad de niños menores de cinco años. Pero, para nuestra próxima tarea, queremos crear datos que se ajusten a muchas tablas donde cada tabla contiene dos columnas. Es decir, año de mayor mortalidad y grupo de edad más afectado.

Además, cada uno de estos conjuntos de datos debe relacionarse con un país único. Sin embargo, después del ejercicio anterior, eso no es tan difícil como podría pensar. Podríamos lograr la agrupación multinivel con un código tan conciso como este:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Test
public void shouldCreateMultiLevelGroup() {
    Map result = stats.stream().collect(
        Collectors.groupingBy(
            CountryStats::getCountry,
            TreeMap::new,
            Collectors.groupingBy(
                CountryStats::getYear,
                TreeMap::new,
                Collectors.collectingAndThen(
                    Collectors.maxBy(
                        Comparator.comparing(
                            CountryStats::getHighest
                        )
                    ),
                    stat -> {
                        return stat
                            .orElseThrow(exc)
                            .getHighest()
                            .getAgeGroup();
                    }                  
                )
            )
        )
    );
    
    System.out.println(result);
}

Aquí, la única diferencia es que hemos incluido una operación groupingBy() externa adicional. Esto garantiza que la recopilación se produzca para cada país por separado. El groupingBy() interior ordena los datos del país por año. Luego, la operación collectingAndThen() utiliza el recopilador de flujo descendente maxBy(). Este recopilador extrae los CountryStats con la mayor mortalidad en todos los grupos de edad.

Y en el paso final encontramos el nombre del grupo de edad con mayor mortalidad. Una vez hecho esto, obtenemos una salida como esta en la consola:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{
Afghanistan={
    1990=Under 5 yrs,
    1991=Under 5 yrs,
    1992=Under 5 yrs,
    ...,
    2014=Under 5 yrs,
    2015=Under 5 yrs,
    2016=Under 5 yrs,
    2017=Under 5 yrs
},
Albania={
    1990=Over 70 yrs,
    1991=Over 70 yrs,
    1992=Over 70 yrs,
    ...,
    2014=Over 70 yrs,
    2015=Over 70 yrs,
    2016=Over 70 yrs,
    2017=Over 70 yrs
},
..,
Congo={
    1990=Between 50 and 69 yrs,
    1991=Between 50 and 69 yrs,
    1992=Between 50 and 69 yrs,
    ...,
    2014=Over 70 yrs,
    2015=Over 70 yrs,
    2016=Over 70 yrs,
    2017=Between 50 and 69 yrs}
...
}

Uso de recopilación y luego() en particiones {#uso de recopilación y luego en partición}

Es posible que nos encontremos con un caso de uso en el que queramos saber qué país está en el límite. Lo que significa que muestra indicios de sufrir tasas de mortalidad inaceptables. Supongamos que la tasa a la que la mortalidad se convierte en un punto importante de preocupación es de 100.000.

{.icon aria-hidden=“true”}

Nota: Esta es una tarifa arbitraria, establecida con fines ilustrativos. En general, el riesgo se calcula por el número de muertes por 100.000, dependiendo de la población del país.

Un país que disfruta de una tasa inferior a esta demuestra que está mitigando el factor de riesgo dado. Está haciendo algo con respecto a la contaminación interior, por ejemplo. Pero, un país cuya tasa está cerca o en esa tasa muestra que podría necesitar alguna ayuda:

Partitioning using collectingAndThen

Aquí, nuestro objetivo es encontrar una manera de dividir los datos de mortalidad en dos. La primera parte contendría los países cuyas tasas aún no han alcanzado el punto de preocupación (x). Pero buscaremos el país cuya tasa sea máxima en este grupo. Este será el país que identificaremos como necesitado de ayuda.

La segunda partición contendrá los países que están experimentando tasas muy altas. Y su máximo será el país/región con las peores tasas. La mejor operación de recopilación para esta tarea sería el método partitioningBy().

Según su [javadoc oficial](https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html#partitioningBy-java.util.function.Predicate-java.util. stream.Collector-), partitioningBy():

Devuelve un Collector que divide los elementos de entrada de acuerdo con un Predicate, reduce los valores de cada partición de acuerdo con otro Collector y los organiza en un Map<Boolean, D> cuyos valores son el resultado de la reducción aguas abajo.

If you'd like to read more about partitioningBy() read our Java 8 Streams: Guía definitiva para particionarBy()!

Siguiendo esto, necesitamos un ‘Predicado’ que verifique si la mortalidad excede 100,000:

1
2
3
4
5
Predicate p = cs -> {
    return cs.getHighest()
        .getMortality()
        .doubleValue() > 100_000
};

Luego, necesitaremos un Collector que identifique los CountryStats que no cumplen con el predicado. Pero también necesitaríamos conocer los CountryStats que no cumplen la condición; pero, es el más alto. Este objeto será de interés porque estaría a punto de alcanzar la tasa de punto de preocupación.

Y como habíamos visto antes, la operación capaz de tal recolección es maxBy():

1
2
3
Collector c = Collectors.maxBy(
    Comparator.comparing(CountryStats::getHighest)
);

Aún así, queremos valores simples de CountryStats en el Map que producirá partitioningBy(). Sin embargo, con maxBy() solo obtendremos una salida de:

1
Map<Boolean, Optional<String>> result = doPartition();

Por lo tanto, confiaremos en collectingAndThen() para adaptar el Collector que emite maxBy():

1
2
3
4
5
6
Collector c = Collectors.collectingAndThen(
    Collectors.maxBy(),
    s -> {
        return s.orElseThrow(exc).toString();
    }
);

Y cuando combinamos todas estas piezas de código, terminamos con:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Test
public void shouldCreatePartition() {
    Map result = stats.stream().collect(
        Collectors.partitioningBy(
            cs -> {
                return cs
                    .getHighest()
                    .getMortality()
                    .doubleValue() > 100_000;
            },
            Collectors.collectingAndThen(
                Collectors.maxBy(
                    Comparator.comparing(
                        CountryStats::getHighest
                    )
                ),
                stat -> {
                    return stat
                        .orElseThrow(exc)
                        .tostring();
                }
            )
        )
    );
    
    System.out.println(result);
}

Al ejecutar este método, obtenemos el resultado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
{
    false={
        country/region=Eastern Sub-Saharan Africa,
        year=1997, 
        mortality={
            ageGroup=Under 5 yrs,
            rate=99830.223
        }
    },
    true={
        country/region=World,
        year=1992,
        mortality={
            ageGroup=Over 70 yrs,
            rate=898396.486
        }
    }
}

Estos resultados significan que la región subsahariana aún no ha llegado al punto de preocupación. Pero, podría golpearlo en cualquier momento. De lo contrario, no nos preocupa el conjunto "Mundo" porque ya ha excedido la tasa establecida, debido a que se ha corregido.

Conclusión

La operación collectingAndThen() hace posible encadenar los resultados de Collector con funciones adicionales. Puede anidar tantos métodos collectingAndThen() entre sí. Otras operaciones, que devuelven tipos Collector, también pueden funcionar con este enfoque de anidamiento.

Cerca del final de este artículo, descubrimos que puede mejorar la presentación de datos. El método también nos permitió refactorizar operaciones ineficientes como sort(). Usando JMH, medimos y descubrimos qué tan rápido puede ejecutarse collectingAndThen().

Encuentre el código completo que ha utilizado este artículo en este repositorio GitHub.

Siéntete libre de clonar y explorar el código en su totalidad. Profundice en los casos de prueba, por ejemplo, para tener una idea de los muchos usos de collectingAndThen().