Google Guava: colecciones con programación funcional en Java 6/7

No hace falta disponer de Java8 para que puedas usar la Programación Funcional a las colecciones. Usando la librería Guava de Google es posible. En este tutorial te lo contamos.

0.Índice de contenidos

1.Introducción

Como seguro que ya sabes, una de las novedades de Java8 es la posibilidad de emplear la programación funcional para tratar con, entre otras cosas, listas en Java.

(Si quieres saber más sobre Java8 te recomiendo que te consultes este tutorial sobre lambdas en Java8, o mucho mejor, te apuntes a nuestro curso de Java 8)

Pero no todo el mundo tiene la posiblidad de trabajar sobre un proyecto Java8. Lo más normal es que los que toman las decisiones no se arriesguen a utilizar la última versión de Java (mal hecho) y nos tengamos que conformar con versiones anteriores como Java6 o Java7 que no tienen estas capacidades. Si estás en esta situacion no te preocupes, hay salida para todo :).

Google puso hace tiempo a disposición de la comunidad una librería de utilidades para Java llamada Guava (sí, G(J)ava), que permite, entre otras cosas, recorrer y tratar Colecciones de una forma más o menos funcional: sin usar bucles para entendernos. Y claro está, es compatible con Java6 y Java7, asi que no tienes excusa para pasarte al lado funcional, aunque sea de forma un poco artificial.

2. Entorno

Para realizar este tutorial se ha empleado el siguiente entorno de desarrollo:

  • Hardware: Mac Book Pro 15″ Intel Core i7 2,8 GHz, 16 GB RAM.
  • Sistema Operativo: Mac OS X El Capitán.
  • Software:Java: 1.7.43; maven 3 y Git si quieres probar los ejemplos.

No obstante, cualquier máquina en la que ejecutes con Java es suficiente.

3. Instalación

Vamos a partir de un proyecto de Maven, así que simplemente deberemos expresar la dependencia de Guava en el pom.xml

Para encontrar la dependencia vamos a MavenCentral y buscamos la librería de Guava. Nos indicará este artefacto: http://mvnrepository.com/artifact/com.google.guava/guava. En el momento de escribir este tutorial, la última versión era la 19, así que la elegimos. El código que habrá que incluir en el pom.xml en el apartado de dependencies es el siguiente:

Para esta demostración también vamos a usar test, asi que aprovechamos y buscamos la dependencia de JUnit4 para poder insertarla. La puedes sacar de aquí:http://mvnrepository.com/artifact/junit/junit/4.12. No obstante todo el código que se expone en este tutorial lo puedes encontrar en un repositorio de mi cuenta de GitHub: https://github.com/4lberto/guavaLists por si quieres experimentar por tu cuenta.

4. Conceptos iniciales: Predicados y Funciones

Antes de ponernos a recorrer listas tenemos que conocer dos conceptos básicos que maneja esta librería y que le permiten modelar el paradigma de la programación funcional: los predicados y las funciones.

Básicamente lo que hace Guava es tomar una lista y aplicar o bien predicados o bien funciones que nosotros hemos diseñado. Es como una encapsulación de operación básica que se aplica a cada elemento de la lista para ver que cumple una condición o transformarlo.

Si estás familiarizado con los patrones de diseño de la programación orientada a objetos, podríamos decir que es una especie de patrón strategy en el cual se extrae la operación a una clase específica (la función o procedimiento), que un iterador especial se dedica a aplicar a cada elemento.

Mejor lo vemos en código:

4.1. Predicados

Un predicado es un método que recibe un objeto por parámetro y devuelve un valor de tipo boolean (true o false). Así de simple.

Esta es su interfaz:

Y esto es un predicado que dado un número entero dice si es mayor que 10:

¿Para qué se utilizan los predicados?

Se aplican a los elementos de una colección o algo que sea iterable, y decidir si cumplen una condición o no. De este modo se puede decidir sobre si se incluyen en el resultado de la operación o no. Por ejemplo:

  • Si un número es par.
  • Si un cadena de texto tiene más de una longitud determinada.
  • Si un objeto tiene un parámetro que cumple la condición.
  • Si un elemento es nulo.
  • Si un elemento está en una lista

Así, si tenemos una lista con objetos, podremos filtrarlos por el cumplimiento o no de un atributo. Nos será muy útil para descartar elementos y crear listas nuevas que solamente tengan los objetos que nos interesan.

4.2. Funciones

Las funciones son métodos que toman un objeto de entrada y producen un objeto de salida.

Esto es una función que dada una cadena de texto, elimina el último caracter, devolviendo una nueva cadena de texto a la salida:

¿Para qué se utilizan las funciones?

Son las operaciones de transformación de una lista de objetos en otra lista de objetos. Así se puede por ejemplo:

  • Sumar una determinada cantidad a cada elemento.
  • Simplificar un objeto para convertir la lista en una lista de identificadores de objetos o un campo que nos interse para hacer una operación posterior.
  • Completar objetos o convertirlos en otros, generando una lista nueva más completa.
  • O incluso si la salida de la función es un Boolean, tendremos una especie de predicado.

Una lista a la que se le aplica una función genera una lista nueva de igual longitud. Ya podremos aplicarles predicados u otras funciones especiales para recortarlas si fuera necesario.

Más adelante veremos las funciones y los predicados en más detalle:

5. Aplicando Programación Funcional a las colecciones: FluentIterable

Ya sabemos de qué van los predicados y las funciones. Ahora sólo queda aplicarlas.

Hay diferentes formas de hacerlo en Guava, empleando los diferentes mecanismos como las clases de utilidades com.google.common.collect.Lists o com.google.common.collect.Collections2 por ejemplo.

Pero para facilitar el manejo en Guava tenemos com.google.common.collect.FluentIterable<E> que nos da una interfaz común a los métodos más usados (realmente en su código luego hace referencia a estas librerías). Además, esta clase está preparada para operar como una Interfaz fluida para que el código sea más legible

A FluenIterable simplemente se le indica a través del método .from() el iterable sobre el que se va a operar, y a continuación las operaciones que se le aplican, siempre siguiente su API fluida. En su propia documentación nos indican un ejemplo:

Vamos a ver con unos test de ejemplo cómo podemos usarla. Antes unas advertencias:

* Puedes descargar los ejemplos y jugar con ellos desde: https://github.com/4lberto/guavaLists.

** Antes de cada test hay una lista de Strings generada con clases de utilidad llamada listOfStrings.

*** Uso test para las pruebas por simpleza a la hora de probar los ejemplos y comprobrar resultado. Obviamente el código explicado se puede emplear en cualquier clase de Java.

5.1. Dada una lista desordenada de Strings, devuelve una lista con los elementos que comienza por “A”

Como podrás ver se ha generado un Predicado que se aplica a cada elemento de entrada, que es de tipo String. El predicado devuelve un boolean, que será true si la palabra empieza por “a” y falso en otro caso.

A continuación, y aquí está la potencia de la programación funcional, se aplica sobre la entrada, que es “listOfStrings” y que ha sido generada anteriormente (bajate los fuentes para verlo).

Gracias a la API Fluida de FluenIterable, cualquiera puede ver leyendo en inglés lo que hace:

Es decir, partiendo de listOfStrings, aplica un filtro, que será el predicado y la salida la convierte en una lista, que además será de tipo com.google.common.collect.ImmutableList, que es un tipo de lista inmutable de alto rendimiento (mejor para el sincronismo y la programación funcional).

Luego aplico unos asserts para comprobar que da el resultado que quiero. Como he dicho antes, uso test para que cuadre en la demo, pero se podría usar en cualquier lugar.

5.2. Devolver la primera palabra de una lista que empiece por la letra “j”

Gracias a la API fluida de FluentIterable podemos hacerlo fácilmente. Simplemente creamos un predicado y lo aplicamos. A la salida en vez de una lista le decimos que nos devuelva un único elemento con .get

Una de las ventajas de tener un predicado (o función) fuera del recorrido de los elementos, es poder reutilizar el código del predicado. Imagina que puedes tener una factoría de predicados o funciones separada y utilizarla a lo largo de tu código en cualquier lugar.

5.3. Añadir un sufijo a cada palabra de una lista

Simplemente creando una función que dado un String nos devuelva el String más el sufijo podemos hacerlo fácilmente. Luego la función se aplica a una lista de Strings que tenemos preparada:

Fijate que ahora se ha empleado la operacion .transform(function) para que transforme los elementos de la lista, y la salida se ha pedido que sea una lista con .toList().

5.4. Ordernar una lista de palabras

No puede ser más fácil y expresivo con la operación .toSortedList pasando por parámetro otra utilidad de Guava, en este caso Ordering.natural(), que devuelve un Comparable preparado para devolver los elementos ordenados de modo natural (así no tenemos que implementar una clase anónima al vuelo)

Como el resultado es de tipo ImmutableList<E> entonces podemos aplicar otro método de la API fluida de este miembros de Guava, como por ejemplo subList(0,10) para seleccionar los 10 primeros elementos.

Si lo hubiéramos hecho dentro de un fluentIterable, podríamos haber empleado .limit(10) para obtener sólo 10 elementos de la lista.

5.5. Predicado parametrizado en clase anónima: Intersección de listas

Quizá estos ejemplos te hayan contentado, pero puede que haya una cosa que no te termine de cuadrar: los predicados sólo admiten elementos de la lista como entrada, y las funciones igual. Si queremos complicar las condiciones admitiendo otros parámetros… ¿cómo podemos hacerlo?

En este ejemplo queremos que, dada una lista de Strings, nos devuelva otra lista con aquellos elementos que están en otra lista. Para ello definiremos el predicado para que tenga acceso a las variables del scope del método en el que se define.

De este modo, dentro de la definición del predicado y de su método apply, haremos uso de variables que no están definidas de forma explícita en el predicado. Mejor con código:

En este ejemplo, dentro del predicado se hace referencia a listTwo, que es una variable a nivel de método del test. Al definirse la clase anónima que implementa la interfaz Predicate de Guava, por la especificación del scope del Java Language Specification, se puede hacer uso de ello.

De nuevo, gracias a FluenIterable de Guava, aplicando un predicado con filter y pidiendo que el resultado sea una lista con toList, se simplifica bastante el matching de ambas listas para encontrarr la intersección.

Pero siempre hay algo que objetar. Quizá eres de la idea inicial que comentaba más atrás se crear unas funciones/predicados que fuesen más o menos reutilizables. En este caso, nuestro predicado únicamente se puede emplear dentro de ese test, ya que tiene una dependencia clara de la lista anterior. Vamos a ver alguna alternativa.

5.6. Predicado parametrizado en nueva clase: Intersección de listas

Un predicado en Guava se trata simplemente de implementar la interfaz Predicate, asi que no hay nada que nos impida darle un poco más de funcionalidad con tal de que el apply siga funcionando tal y como figura en el contrato de la interfaz.

Ahora creamos una clase estática dentro de la clase de tests. No hace que falta que sea estática per se, sino que para simplificar no voy a crear un fichero Java nuevo, de modo que la introduzco dentro de la clase de test. Sácala a un fichero nuevo si crees conveniente:

Ásí tenemos nuestro nuevo predicado creado llamado PredicateMatchingList. Así podremos reutilizarlo en otros desarrollos de iterable. El test ahora quedaría así:

Aún podemos darle una vuelta de tuerca más generando predicados con un método Factory, más acorde con las buenas prácticas de desarrollo.

Por cierto, en la clase com.google.common.base.Predicates hay factorías para este tipo de predicados, como por ejemplo in(Collection<? extends T> target), que nos fabricaría un predicado similar al anterior.

Simplemente cambiaríamos la forma de obtener el predicado por:

5.7. Predicado parametrizado desde un método Factory: números mayor que el parámetro.

Este ejemplo es similar al anterior. Queremos un predicado que nos diga si el elemento sobre el que se aplica es mayor que un número dado por parámetro. Ya sabes las otras dos alternativas anterior. Vamos ahora con un método fáctory que nos genere el predicado.

Si te fijas bien, es una especie de mezcla entre crear una clase con un constructor con el parámetro y otra de tomar la variable del scope: el valor “number” se aplica dentro del nuevo predicado generado porque forma parte del scope del método factory.

Aplicarlo es tan fácil como esperamos:

Por cierto, estos ejemplos para predicados también son perfectamente aplicables a las funciones de Guava (y a cualquier cosa que implemente una interfaz, claro) :).

5.8. Suma de precios de los 2 coches más potentes

Seguro que has oído hablar de map-reduce en bases de datos NoSQL, que al final aplican funciones a listados de datos.

  • Map: se encarga de seleccionar y transformar los elementos buscados. Por ejemplo ordena los coches por potencia y devuelve los dos primeros de la lista.
  • Reduce: realiza una operación de “resumen”, como por ejemplo la media o una suma. En nuestro caso será la suma del precio.

Ya sabes que lo que estamos haciendo hasta ahora se corresponde con el Map, pero nos falta una operación que haga “reduce”, es decir, que recorra los elementos de una lista y haga una operación acumulativa que devuelva un único resultado. ¿Cómo se hace en Guava?.

La respuesta es… No se puede hacer en Guava la operacion reduce a menos que la implementemos nosotros.

Y la forma de implementarlo nosotros es a través de un tradicional bucle que recorra el resultado del Map. O si somos más sofisticados hagamos nuestra propia operación reduce que tome una lista y una operación de dos elementos como parámetro y lo aplique en parejas: el 1 con el 2, el resultado al 3, y así sucesivamente.

Así pues, nosotros aplicaremos un bucle para conocer el precio acumulado de los coches más potentes:

Al menos Guava nos ha hecho la vida más sencilla para ordenar y sacar los dos coches más potentes… pero no es una librería tan potente como la parte de streams de Java8 🙁

.

6. Extra: Predicados y Funciones extendidos

Como hemos visto, al final Guava, y la programación funcional, nos permite desacoplar en funciones y predicados la lógica que se aplica a los elementos, lo cual es una gran ventaja frente a los bucles.

En esta sección “extra” vamos a ver algunas curiosidades de las funciones y predicados que nos pueden ser de utilidad.

6.1. Extra Predicados

La clase com.google.common.base.Predicates contiene algunos predicados generales prefabricados, como por ejemplo:

6.1.1. Predicados básicos: true, false

Serán utilizables por ejemplo en operaciones de composición booleana de predicados. Así tenemos:

6.1.2. Predicado para filtar por clase

Si tenemos la necesidad de, dada una lista, quedarnos con los elementos que son de una clase determinada (o sus descendientes), podemos aplicar Predicates.assignableFrom.

En este ejemplo filtramos por clases que extiendan la clase java.lang.Number:

También existe el predicado Predicate.instanceOf para una clase exacta.

6.1.3. Composición de Predicados

Ya sabemos que un predicado se aplica a una clase y devuelve un boolean. Parece lógico que se puedan combinar predicados simples para generar predicados más complejos con operaciones booleanas de composición: or, and, not.

A continuación vemos un ejemplo en el que hay dos predicados: uno que evalúa que un Integer sea mayor que 10 y otro que evalúa que sea menor que 20. La combinación de ambos con una operación “and” debería darnos un predicado que fuese verdadero cuando el número proporcionado esté entre 10 y 20:

De igual modo existe la generación de predicados a partir de las operaciones “or” y “not”.

Simplemente ten en cuenta que se puede ampliar el campo de cosas que se pueden hacer con los predicados y por tanto con Guava.

6.2. Extra Funciones.

Igual que los predicados, existe com.google.common.base.Functions, que tiene métodos estáticos de factoría para crear funciones a partir de parámetros.

Como las funciones reciben una clase de entrada y dan otra clase de salida, no tienen esas operaciones booleanas de los Predicados, pero tienen alguna función interesante:

6.2.1. Composición de Funciones

Como en las matemáticas: si F(A)->B y F(B)->C, hay una composición de funciones que nos dará F(A)->F(C). Esto se puede hacer con el método estático compose(Function<B,C> g, Function<A,? extends B> f).

Se tengo una función que elimina el primer caracter de una cadena de texto y otra que elimina el último caracter, con la composición tendré una función que elimina el primer y el último caracter de una cadena de texto. Aquí vemos un ejemplo:

6.2.2. Función asignación a través de un Mapa

Finalmente vamos a ver una función de la factoría de com.google.common.base.Functions que me parce muy interesante: permite transformar el objeto de entrada en uno de salida, pero la regla de transformación está determinada en un mapa. En este mapa, las claves (keys) son los obejtos de entrada, y los valores (values) los de salida.

Para ello se emplea forMap(Map<K,? extends V> map, V defaultValue), que recibe el mapa de corrspondencias por parámetro y un valor en el caso de que no se encuentre la entrada.

En el siguiente ejemplo se transforman los códigos de locale (en, es, rus) en el idioma correspondiente (inglés, español, ruso)…

6.2.3. Función desde un predicado

Finalmente como hemos ido viendo a lo largo de este tutorial, sabemos que un predicado es una función que devuelve un boolean, así que es posible crear una función a partir de un predicado con Functions.forPredicate. Muy sencillo el ejemplo:

7. Conclusiones

La programación funcional está cambiando la forma de programar en los últimos años. Como consecuencia Java ha tenido que implementar este tipo de programación aplicada a streams y lambdas en la versión Java8. Desafortundamente no todo el mundo tiene la posibilidad de trabajar con Java8 y tiene que conformarse con Java6 o Java7. Para estos casos se puede emplear la librería Guava de Google.

Guava nos proporciona algunas utilidades para iterar sobre colecciones de objetos (listas, conjuntos…) de una forma similar a la programación funcional. Para ello utiliza una API fluida en la que se indican funciones y predicados que se aplican a los elementos de las colecciones para poder transformar y filtrar respectivamente. De este modo podemos desarrollar nuestro código de una forma similar a Java8 con streams y lambdas, aunque no en todo su esplendor, ya que tiene algunas lagunas como por ejemplo la operación reduce.