Hemos utilizado el método Collect() varias veces antes para combinar los elementos devueltos por Stream en una ArrayList. Esta es una operación de reducción, que resulta útil para convertir una colección en otro tipo (normalmente una colección mutable). La función Collect(), si se usa en combinación con algunos métodos de la clase de herramienta Collectors, puede proporcionar una gran comodidad, como presentaremos en esta sección.
Sigamos usando la lista de personas anterior como ejemplo para ver qué puede hacer el método Collect(). Supongamos que queremos encontrar a todas las personas mayores de 20 años de la lista original. Aquí está la versión implementada usando mutabilidad y el método forEach():
Copie el código de código de la siguiente manera:
Lista<Persona> mayorThan20 = nueva ArrayList<>() personas.stream();
.filtro(persona -> persona.getAge() > 20)
.forEach(persona -> mayores de 20.add(persona)); System.out.println("Personas mayores de 20 años: " + mayores de 20);
Usamos el método filter() para filtrar de la lista a todas las personas mayores de 20 años. Luego, en el método forEach, agregamos elementos a un ArrayList que se ha inicializado previamente. Primero echemos un vistazo al resultado de este código y luego reconstruyémoslo más tarde.
Copie el código de código de la siguiente manera:
Personas mayores de 20 años: [Sara - 21, Jane - 21, Greg - 35]
El resultado del programa es correcto, pero todavía hay un pequeño problema. Primero, agregar elementos a una colección es una operación de bajo nivel: es imperativa, no declarativa. Si queremos transformar esta iteración en concurrente, debemos considerar los problemas de seguridad de los subprocesos: la variabilidad dificulta la paralelización. Afortunadamente, este problema se puede resolver fácilmente utilizando el método Collect(). Veamos cómo se logra esto.
El método Collect() acepta un Stream y lo recopila en un contenedor de resultados. Para ello es necesario saber tres cosas:
+ Cómo crear un contenedor de resultados (por ejemplo, usando el método ArrayList::new) + Cómo agregar un solo elemento al contenedor (por ejemplo, usando el método ArrayList::add) + Cómo fusionar un conjunto de resultados en otro (por ejemplo, usando el método ArrayList: :addAll)
El último elemento no es necesario para operaciones en serie; el código está diseñado para admitir operaciones en serie y en paralelo.
Proporcionamos estas operaciones al método de recopilación y le permitimos recopilar la secuencia filtrada.
Copie el código de código de la siguiente manera:
Lista<Persona>mayorDe20 =
personas.stream()
.filtro(persona -> persona.getAge() > 20)
.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
System.out.println("Personas mayores de 20 años: " + mayores de 20);
El resultado de este código es el mismo que antes, pero escribirlo de esta manera tiene muchas ventajas.
En primer lugar, nuestro método de programación es más centrado y expresivo, y transmite claramente el propósito de recopilar los resultados en una ArrayList. El primer parámetro de Collect() es una fábrica o productor, y el parámetro siguiente es una operación utilizada para recopilar elementos.
En segundo lugar, dado que no realizamos modificaciones explícitas en el código, podemos realizar fácilmente esta iteración en paralelo. Dejamos que la biblioteca subyacente maneje las modificaciones y ella se encargará de los problemas de coordinación y seguridad de los subprocesos, aunque ArrayList en sí no es seguro para subprocesos: buen trabajo.
Si las condiciones lo permiten, el método Collect() puede agregar elementos a diferentes sublistas en paralelo y luego fusionarlos en una lista grande de forma segura para subprocesos (el último parámetro se usa para la operación de fusión).
Hemos visto que hay muchos beneficios al usar el método Collect() en lugar de agregar elementos manualmente a una lista. Veamos una versión sobrecargada de este método; es más simple y conveniente: toma un recopilador como parámetro. Este Collector es una interfaz que incluye productores, sumadores y combinadores. En versiones anteriores, estas operaciones se pasaban a métodos como parámetros independientes. El uso de Collector es más simple y se puede reutilizar. La clase de herramienta Collectors proporciona un método toList que puede generar una implementación de Collector para agregar elementos a una ArrayList. Modifiquemos el código anterior y usemos el método Collect().
Copie el código de código de la siguiente manera:
Lista<Persona>mayorDe20 =
personas.stream()
.filtro(persona -> persona.getAge() > 20)
.collect(Collectors.toList());
System.out.println("Personas mayores de 20 años: " + mayores de 20);
Se utiliza una versión concisa del método Collect() de la clase de herramienta Collectors, pero se puede utilizar de más de una forma. Hay varios métodos diferentes en la clase de herramienta Collectors para realizar diferentes operaciones de recopilación y suma. Por ejemplo, además del método toList(), también existe el método toSet(), que se puede agregar a un conjunto, el método toMap(), que se puede usar para recopilar en un conjunto clave-valor, y el método toMap(), que se puede usar para recopilar en un conjunto clave-valor. método join(), que se puede unir en una cadena. También podemos combinar métodos como mapeo(), recopilandoAndThen(), minBy(), maxBy() y groupingBy() para su uso.
Usemos el método groupingBy() para agrupar personas por edad.
Copie el código de código de la siguiente manera:
Mapa<Entero, Lista<Persona>> personasPorEdad =
personas.stream()
.collect(Collectors.groupingBy(Persona::getAge));
System.out.println("Agrupado por edad: " + peopleByAge);
Simplemente llame al método Collect() para completar la agrupación. groupingBy() acepta una expresión lambda o una referencia de método (esto se denomina función de clasificación) y devuelve el valor de un determinado atributo del objeto que debe agruparse. Según el valor devuelto por nuestra función, los elementos en el contexto de llamada se colocan en un grupo determinado. Los resultados de la agrupación se pueden ver en el resultado:
Copie el código de código de la siguiente manera:
Agrupados por edad: {35=[Greg - 35], 20=[John - 20], 21=[Sara - 21, Jane - 21]}
Las personas han sido agrupadas por edades.
En el ejemplo anterior agrupamos a las personas por edad. Una variante del método groupingBy() puede agrupar según múltiples condiciones. El método simple groupingBy() utiliza un clasificador para recopilar elementos. El recopilador general groupingBy() puede especificar un recopilador para cada grupo. En otras palabras, los elementos pasarán por diferentes clasificadores y colecciones durante el proceso de recopilación, como veremos a continuación.
Siguiendo con el ejemplo anterior, esta vez en lugar de agrupar por edad, simplemente obtenemos los nombres de las personas y las ordenamos por edad.
Copie el código de código de la siguiente manera:
Mapa<Entero, Lista<Cadena>> nombreDePersonasByAge =
personas.stream()
.recolectar(
groupingBy(Persona::getAge, mapeo(Persona::getName, toList())));
System.out.println("Personas agrupadas por edad: " + nameOfPeopleByAge);
Esta versión de groupingBy() acepta dos parámetros: el primero es la edad, que es la condición para la agrupación, y el segundo es un recopilador, que es el resultado devuelto por la función mapeo(). Todos estos métodos provienen de la clase de herramienta Collectors y se importan estáticamente en este código. El método mapeo() acepta dos parámetros, uno es el atributo utilizado para el mapeo y el otro es el lugar donde se recopilarán los objetos, como una lista o un conjunto. Echemos un vistazo al resultado del código anterior:
Copie el código de código de la siguiente manera:
Personas agrupadas por edad: {35=[Greg], 20=[John], 21=[Sara, Jane]}
Como puede ver, los nombres de las personas se han agrupado por edad.
Veamos nuevamente una operación combinada: agrupe por la primera letra del nombre y luego seleccione la persona de mayor edad en cada grupo.
Copie el código de código de la siguiente manera:
Comparador<Persona> porEdad = Comparador.comparing(Persona::getEdad);
Mapa<Carácter, Opcional<Persona>>personamásantiguaDeCadaLetra =
personas.stream()
.collect(groupingBy(persona -> persona.getName().charAt(0),
reduciendo(BinaryOperator.maxBy(byAge))));
System.out.println("Persona de mayor edad de cada letra:");
System.out.println(personamásantiguaDeCadaLetra);
Primero ordenamos los nombres alfabéticamente. Para lograr esto, pasamos una expresión lambda como primer parámetro de groupingBy(). Esta expresión lambda se utiliza para devolver la primera letra del nombre para la agrupación. El segundo parámetro ya no es mapeo (), pero realiza una operación de reducción. Dentro de cada grupo, utiliza el método maxBy() para derivar el elemento más antiguo de todos los elementos. La sintaxis parece un poco exagerada debido a las muchas operaciones que combina, pero todo se lee así: Agrupe por la primera letra del nombre y luego trabaje hasta la mayor del grupo. Considere el resultado de este código, que enumera a la persona de mayor edad en un grupo de nombres que comienzan con una letra determinada.
Copie el código de código de la siguiente manera:
Persona de mayor edad de cada letra:
{S=Opcional[Sara - 21], G=Opcional[Greg - 35], J=Opcional[Jane - 21]}
Ya hemos experimentado el poder del método Collect() y la clase de utilidad Collectors. En la documentación oficial de su IDE o JDK, tómese un tiempo para estudiar la clase de herramienta Collectors y familiarizarse con los diversos métodos que proporciona. A continuación usaremos expresiones lambda para implementar algunos filtros.