Nuevos Collectors añadidos después de Java 8
Los collectors han sido muy populares dentro de los desarrolladores desde que fueran introducidos en Java 8. Especialmente los que vienen predefinidos en la clase Collectors han resultado ser muy útiles para las operaciones más comunes que pueden surgir cuando trabajamos con streams.
Por suerte, en las releases de Java posteriores a Java 8 se han introducido nuevos collectors. En las siguientes secciones, revisaremos cada una de las releases que contiene nuevos collectors y veremos como nos pueden ayudar.
Java 9
En Java 9 se han introducido 2 collectors nuevos que nos simplifican algunas operaciones cuando trabajamos con streams. Los collectors que se han añadido son flatMapping() y filtering(). ¡Veamos como funcionan!
flatMapping()
Este collector es bastante similar a la operación flatMap() de un Stream. Acepta los elementos de un stream como parámetro y produce otro stream que será la entrada de un downstream collector que tenemos que especificar.
Para ilustrar esto con unos ejemplos vamos a crear una clase Email:
Y a continuación creamos una lista de emails:
Un ejemplo sencillo para usar este collector sería obtener todos los destinatarios de los emails en un Set:
Sin embargo, este collector no es de mucha utilidad en este ejemplo, ya que podedmos obtener el mismo resultado sin necesidad de usarlo:
En realidad, la principal intención de este collector fue la de simplificar las reducciones multinivel de un stream cuando usamos otros collectors como groupingBy() o partitioningBy().
Veamos ahora un ejemplo más apropiado para ver la utilidad de este collector. Para ello, vamos a modificar el ejemplo anterior para obtener los destinatarios de los emails, pero agrupados por el remitente o persona que envía el email:
Como podemos ver, primero agrupamos los emails por remitente y luego obtenemos los destinatarios en un Set. Para hacer esto último, hacemos uso de flatMapping() y le pasamos una función que convierte la lista de destinatarios en un stream. Por último, este stream es utilizado por toSet() para crear el set de destinatarios.
Gracias a este collector no tenemos que preocuparnos por la conversión de la lista a stream y nuestro collector es mucho más compacto.
filtering()
El collector filtering() nos permite filtrar los elementos de un stream antes de pasarlos a un downstream collector. Al igual que antes, el objetivo de este collector es simplificar las reducciones multinivel.
¡Volvamos a nuestra lista de emails y veamos como este collector puede ayudarnos! Supongamos que queremos obtener todos los emails que tienen a alguien en copia y agruparlos por remitente. Antes de Java 9 podríamos hacer algo como esto:
Pero como estamos filtrando antes de agrupar, no obtenemos las personas que no tienen emails con alguien en copia:
Sin embargo, podemos modificar este comportamiento si usamos el collector filtering():
Como podemos observar, ahora filtramos después de agrupar. Por tanto, obtenemos todas las personas en el Map
En este caso, en el resultado también obtenemos a Tim, aunque vemos que no tiene ningún email con personas en copia.
Java 10
Los collectors que se han añadido en Java 10 son relativos a la creación de colecciones que no se puedan modificar una vez creadas. Estas colecciones no permiten añadir, eliminar o reemplazar elementos. Además, las colecciones creadas por estos collectors poseen otras características como no aceptar valores que sean null.
En las siguientes secciones, mostraremos como crear este tipo de colecciones usando los nuevos collectors de Java 10.
toUnmodifiableList()
Antes de Java 10 no era posible utilizar collectors para crear colecciones que no se pudiesen modificar a menos que usásemos algún tipo de workaround. Esto se debe a que la colección donde el collector acumula los elementos tiene que poder modificarse para poder agregar, eliminar o reemplazar elementos durante el proceso de recolección. Por este motivo, Java no lo permite y lanza una UnsupportedOperationException:
Un workaround muy común para solventar este problema es usar collectingAndThen(). De esta forma, justo después de que el collector haya acabado y retornado la colección, podemos transformarla en una que no se pueda modificar:
Otra opción es crear nuestro propio collector. Pero esto es justo lo que se ha hecho en Java 10:
Como podemos ver, funciona de la misma manera que toList().
Si miramos con detalle la implementación de este collector, podemos ver que es prácticamente igual que la de toList() pero haciendo uso de la función finisher del Collector:
La función finisher se encarga de transformar la colección donde se han almacenado los elementos en una que no se pueda modificar justo antes de devolverla.
Este collector es bastante conveniente para evitarnos crear nuestros collectors o usar workarounds para crear este tipo de listas.
toUnmodifiableSet() y toUnmodifiableMap()
Además de toUnmodifiableList(), también se ha añadido otro collector para crear sets que no se puedan modificar:
Y también tenemos uno disponible para hacer lo mismo con maps:
Como podemos observar, son todos muy similares y se usan de la misma forma que los que crean colecciones modificables.
Java 12
Con la llegada de Java 12 nos hemos encontrado con otro collector nuevo: teeing(). Este collector es bastante complejo y puede llegar a ser bastante útil como veremos a continuación.
teeing()
Los collectors han resultado ser muy útiles y fáciles de usar para reducir streams. Sin embargo, hay veces que necesitamos reducir un stream de varias formas a la vez y esto puede convertirse en una tarea tediosa si lo queremos hacer usando el mismo stream. Los collectors de summarizing ya proveen esta funcionalidad, pero son solo útiles para obtener estadísticas de los elementos del stream.
El collector teeing() puede resultar muy práctico en estas situaciones donde queremos coleccionar los elementos de un stream de 2 formas distintas. Acepta como parámetros 2 downstream collectors y una función merger que recibe los resultados de los 2 collectors y devuelve un único resultado.
Vamos a volver a utilizar nuestra lista de emails. Imaginemos que necesitamos saber si una persona manda más emails de los que recibe. Esto es relativamente sencillo de obtener si hacemos uso del collector teeing():
En este ejemplo, el primer collector cuenta cuantos emails ha enviado esta persona y el segundo cuenta cuantos ha recibido. Finalmente, la función merger comprueba cual de las 2 sumas es mayor.
Veamos otro ejemplo. Imaginémonos que ahora queremos saber cual es la diferencia entre el mayor y el menor mensaje de un email:
Esta vez, el primer collector busca el email que tiene el mensaje más largo y el segundo busca el que lo tiene más corto. Por último, en la función merger simplemente obtenemos la diferencia entre ambos.
Como hemos visto, este collector nos proporciona una nueva forma de recolectar resultados de un stream.
Conclusión
En este tutorial hemos visto los nuevos collectors predefinidos que se han añadido en las releases de Java posteriores a Java 8. Hemos repasado cada una de las versiones que contiene nuevos collectors y hemos mostrado ejemplos de como usarlos. Además, también hemos visto como nos pueden ayudar y en qué situaciones son útiles.
El código fuente de los ejemplos se encuentra disponible en github.
Deja un comentario