Java 8 Streams: Guía definitiva para particionarBy()

En esta extensa guía práctica, aprenda cómo particionar flujos y listas en Java con particióningBy(), con recopiladores posteriores y ejemplos de diferentes operaciones de reducción.

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.

Desde la vista de arriba hacia abajo, el método Collectors.partitioningBy() se puede resumir así:

1
2
3
4
5
List<String> names = Arrays.asList("John", "Jane", "Michael", "Anna", "James");
Map<Boolean, List<String>> partitionByNameLength = names.stream()
        .collect(Collectors.partitioningBy(name -> name.length() > 4));

System.out.println(partitionByNameLength);
1
{false=[John, Jane, Anna], true=[Michael, James]}

Sin embargo, hay más en este método que el valor nominal, e incluso puede encadenar recopiladores posteriores además del predicado utilizado para probar los elementos.

¡En esta guía, veremos cómo particionar flujos en Java con Collectors.partitioningBy()!

Coleccionistas y Stream.collect()

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.

Guía para Collectors.partitioningBy()

La clase Collectors es amplia y versátil, y nos permite recopilar flujos de una miríada de formas. Para recolectar elementos, dividiendo el flujo en particiones, dado un cierto predicado - usamos Collectors.partitioningBy().

Tenemos a nuestra disposición dos versiones sobrecargadas del método, pero ambas devuelven un ‘Collector’ que divide los elementos de entrada de acuerdo con un ‘Predicado’ y los organiza en ‘Map<Boolean, List>’.

{.icon aria-hidden=“true”}

El método partitioningBy() siempre devuelve un Mapa con dos entradas: una para cuando el Predicado es verdadero y otra para cuando es falso. Ambas entradas pueden tener listas vacías, pero estarán presentes.

Definamos una clase Student simple para usar en los ejemplos de código:

1
2
3
4
5
6
7
private String name;
private String surname;
private String city;
private double avgGrade;
private int age;

// Constructors, Getters, Setters, toString()

Y haga una lista de estudiantes para particionar más tarde:

1
2
3
4
5
6
7
List<Student> students = Arrays.asList(
        new Student("John", "Smith", "Miami", 7.38, 19),
        new Student("Jane", "Miles", "New York", 8.4, 21),
        new Student("Michael", "Peterson", "New York", 7.5, 20),
        new Student("Gabriella", "Robertson", "Miami", 9.1, 20),
        new Student("Kyle", "Miller", "Miami", 9.83, 20)
);

Collectors.partitioningBy() usando un predicado

En su forma esencial, el método partitioningBy() acepta un predicado:

1
public static <T> Collector<T,?,Map<Boolean,List<T>>> partitioningBy(Predicate<? super T> predicate)

Cada uno de los elementos de Stream se compara con el predicado y, en función del valor booleano resultante, este Collector agrupa los elementos en dos conjuntos y devuelve el resultado como Map<Boolean, List<T>>.

{.icon aria-hidden=“true”}

Nota: No hay garantías sobre el tipo, mutabilidad, serialización o seguridad de subprocesos del ‘Mapa’ devuelto.

Antes de aplicar el método en nuestra lista de estudiantes, intentemos dividir una lista de nombres en función de si su “longitud” supera “4” o no:

1
2
3
4
5
List<String> names = Arrays.asList("John", "Jane", "Michael", "Anna", "James");
Map<Boolean, List<String>> partitionByNameLength = names.stream()
        .collect(Collectors.partitioningBy(name -> name.length() > 4));

System.out.println(partitionByNameLength);

Para cada elemento de la ‘Lista’ que tiene una longitud superior a 4, el predicado devuelve ‘verdadero’ y, de lo contrario, ‘falso’. Según estos resultados, el método partitioningBy() recopila los elementos en consecuencia:

1
{false=[John, Jane, Anna], true=[Michael, James]}

Usar el método en nuestra clase “Estudiante” personalizada realmente no es diferente: solo estamos accediendo a un campo diferente a través de un método diferente. El predicado que usaremos ahora probará nuestros objetos Student por la longitud de su nombre y su calificación promedio:

1
2
3
4
5
Map<Boolean, List<Student>> partitionByNameAvgGrade = students.stream()
    .collect(Collectors.partitioningBy(student->student.getName().length() > 8 
                                       && student.getAvgGrade() > 8.0));

System.out.println(partitionByNameAvgGrade);

Esto dividirá a los estudiantes en dos predicados: si su nombre tiene más de 8 caracteres y si su calificación promedio es superior a 8:

1
2
3
4
{
false=[Student{name='John', surname='Smith', city='Miami', avgGrade=7.38, age=19}, Student{name='Jane', surname='Miles', city='New York', avgGrade=8.4, age=21}, Student{name='Michael', surname='Peterson', city='New York', avgGrade=7.5, age=20}, Student{name='Kyle', surname='Miller', city='Miami', avgGrade=9.83, age=20}], 
true=[Student{name='Gabriella', surname='Robertson', city='Miami', avgGrade=9.1, age=20}]
}

{.icon aria-hidden=“true”}

El predicado puede ser cualquier función o una expresión lambda que devuelva un valor “booleano”.

Si desea leer más sobre interfaces funcionales, funciones lambda y predicados, lea nuestra Guía completa de predicados de Java 8!

Collectors.partitioningBy() mediante un predicado y un recopilador descendente

En lugar de proporcionar solo un predicado, que ya nos brinda bastante flexibilidad en términos de formas de probar objetos, también podemos proporcionar un recopilador posterior.

Este recopilador se puede utilizar para reducir valores en cada partición según otro Collector y organiza el mapa final en un Map<Boolean, D> donde los valores de D son los resultados del recopilador posterior:

1
2
3
public static <T,D,A> Collector<T,?,Map<Boolean,D>> 
    partitioningBy(Predicate<? super T> predicate,
                   Collector<? super T,A,D> downstream)

Echemos un vistazo a algunos recopiladores descendentes diferentes y cómo se pueden usar para permitir que partitioningBy() se use de una manera más versátil. Vale la pena señalar que no hay restricciones reales sobre el tipo de colector que puede usar aquí, siempre que tenga sentido usarlo para su tarea.

Uso de Collectors.mapping() como recopilador descendente

Collectors.mapping() es un recopilador muy común, y podemos realizar un mapeo de los elementos después de particionarlos. Por ejemplo, dividamos un flujo de nombres en función de su longitud, y luego asignemos los nombres a sus contrapartes en mayúsculas y finalmente reunámoslos en una lista:

1
2
3
4
5
6
List<String> names = Arrays.asList("John", "Mike", "Michael", "Joe", "James");
Map<Boolean, List<String>> partitionByNameLength = names.stream()
    .collect(Collectors.partitioningBy(name -> name.length() > 4,
            Collectors.mapping(String::toUpperCase, Collectors.toList())));

System.out.println(partitionByNameLength);

El método Collectors.mapping() se usa como un recopilador descendente, que acepta dos parámetros en sí mismo: un mapeador (función que se aplicará a los elementos de entrada) y su propio recopilador descendente que acepta los valores asignados.

Después de aplicar la función toUpperCase() en cada elemento de la transmisión, los resultados se acumulan y recopilan en una lista:

1
{false=[JOHN, JANE, ANNA], true=[MICHAEL, JAMES]}

El resultado es, naturalmente, el mismo que antes; sin embargo, hemos pasado estas cadenas a través de una función de mapeo transformador.

A continuación, también podemos usarlo en nuestra clase Student:

1
2
3
4
5
6
Map<Boolean, List<String>> partitionStudentsByName = students.stream()
    .collect(Collectors.partitioningBy(student->student.getName().length() > 8
                                       && student.getAvgGrade() > 8.0,
             Collectors.mapping(Student::getName, Collectors.toList())));

System.out.println(partitionStudentsByName);

Aquí, hemos reducido a los estudiantes a sus nombres, en lugar de que el método toString() se haga cargo de los objetos después de recopilarlos en un mapa. De esta manera, podemos formatear la salida mucho mejor que antes, ya que es posible que no queramos extraer la información completa del objeto de todos modos:

1
{false=[John, Jane, Michael, Kyle], true=[Gabriella]}

Usar Collectors.counting() como un recopilador descendente

El colector counting() es otro colector de reducción, que reduce un vector de elementos a un valor escalar: el recuento de elementos en la secuencia.

If you'd like to read more about the counting collector - read our Guía para coleccionistas de Java 8: contar()!

Este recopilador se puede proporcionar fácilmente como el recopilador descendente para contar la cantidad de objetos que pasan el predicado y la cantidad de los que no:

1
2
3
4
5
Map<Boolean, Long> partitionByAvgGrade = students.stream()
    .collect(Collectors.partitioningBy(student->student.getAvgGrade() > 8.0,
             Collectors.counting()));

System.out.println(partitionByAvgGrade);

El par en nuestro Map<K, V> que representa el par clave-valor es un poco diferente al anterior. Hasta ahora, siempre teníamos un <K, V> que se representaba como <Boolean, List<T>> (T es String o Student en nuestros ejemplos), pero ahora estamos usando Long.

Esto se debe a que el método counting() siempre devuelve un Largo, por lo que solo estamos ajustando el mapa en consecuencia:

1
{false=2, true=3}

Similitudes y diferencias entre particionar por() y agrupar por() {#similitudes y diferencias entre particionar por y agrupar por}

Si está familiarizado con la familia de métodos groupingBy() de la misma clase Collectors, es posible que haya notado las similitudes que tiene con partitioningBy(), y quizás se haya preguntado: _qué\ ’s la diferencia real? _

If you aren't familiar with the groupingBy() family of methods, read up about them in our Guía de recopiladores de Java 8: groupingBy()!

groupingBy() tiene tres sobrecargas diferentes dentro de la clase Collectors:

  • Agrupación con una función de clasificación
  • Agrupación con una función de clasificación y un colector aguas abajo
  • Agrupación con Función de Clasificación, Colector Descendente y Suministrador

Sin embargo, las dos primeras son muy similares a las variantes partitioningBy() que ya describimos en esta guía.

El método partitioningBy() toma un Predicado, mientras que groupingBy() toma una Función.

Hemos usado una expresión lambda varias veces en la guía:

1
name -> name.length() > 4

Según el contexto en el que se use, puede servir como Predicado o Función. Los predicados aceptan valores de entrada y devuelven un valor booleano después de aplicar su método test() en la entrada. Funciones aceptan valores de entrada y devuelven un valor transformado, aplicando el método apply() en la entrada.

En ambos casos, los cuerpos de los métodos test() y apply() son la expresión lambda que hemos proporcionado.

Hablemos ahora de las diferencias. El primero importante es que partitioningBy() siempre mapeará con dos entradas, una para la cual la prueba del predicado dio como resultado verdadero, la otra siendo falso. Ambas entradas pueden ser listas vacías y seguirán existiendo. Por otro lado, eso es algo que groupingBy() no hará, ya que solo crea entradas cuando\ son necesarios.

Además, si tenemos un Predicate<T> predefinido, solo se puede pasar al método partitioningBy(). De manera similar, si tenemos una Función<T, Boolean> predefinida, solo se puede pasar al método groupingBy().

Conclusión

En este artículo, hablamos extensamente sobre el método partitioningBy() de la clase Collectors. Mostramos cómo podemos usarlo tanto en una ‘Lista’ simple de ‘Cadenas’ como en una clase más personalizada definida por el usuario.

También mostramos cómo podemos usar diferentes recopiladores posteriores en nuestros ejemplos para lograr una mejor partición de nuestros datos, con listas reducidas en lugar de objetos completos.

Finalmente, discutimos las similitudes y diferencias entre los métodos groupingBy() y partitioningBy(), y qué usos tienen ambos en el código.

Licensed under CC BY-NC-SA 4.0