Introducción a Groovy (III)

Tanto en el primer como en el segundo artículo del tutorial de Groovy vimos los conceptos básicos del lenguage, así como algunas estructuras de datos. En esta tercera entrega vamos a ver dos tipos nativos del lenguaje: Rangos y Colecciones.

3.1 Rangos

Los rangos son un tipo de datos nativo, derivado de List, que nos permite definir una lista de valores secuenciales. Veamos el ejemplo mas simple:

def rango = 1..5
def rango = 1..<5
	

El primer rango arriba definido es una lista con los valores del 1 al 5, ambos inclusive. El segundo rango contiene una lista con los valores del 1 al 4, pues el último valor ha sido excluido. Ahora que sabemos como definir un rango, podemos iterarlo llamando al método each(), el cual acepta un closure como argumento (recuerda que si el único o último parámetro de un método es un closure, podemos definir dicho closure a continuación de la llamada al método):

rango.each { 
    println it 
}
	

En el código anterior, iteramos a través del rango imprimiendo cada uno de sus valores. Si no vamos a reusar el rango, podemos definirlo "al vuelo", insertándolo entre paréntesis:

(1..5).each { 
    println it 
} 
	

Una forma alternativa de iterar a través de los valores de un rango es mediante un bucle for:

for(int contador in 1..5) { 
    println contador 
} 
	

Además de each(), los rangos disponen de una serie de atributos y métodos que podemos llamar en nuestra aplicación:

def rango = 5..10 
println rango.from 
println rango.to 
println rango.contains(4) 
println rango.size() 
println rango.get(3) 
println rango[3] 
	

El atributo from devuelve el valor de inicio del rango, mientras que el atributo to devuelve el último valor. El método contains() devuelve true si su argumento está incluido en el rango, y size() devuelve el tamaño del rango. Por último, podemos obtener el valor almacenado en un índice del rango (recuerda que es una lista) mediante el método get(indice) o mediante notación de arrays (rango[indice]). Otra opción permitida por un rango es la de invertirlo, gracias al método reverse():

def rango = 10..15 
def inverso = rango.reverse() 
inverso.each { 
    println it 
} 
	

Hasta ahora hemos utilizado rangos que contenían valores enteros; sin embargo, cualquier clase que implemente el interface Comparable, así como los métodos next() y previous(), puede ser usado como valor de un rango. Este es el caso, por ejemplo, de Date():

def hoy = new Date() 
def dentroDeSieteDias = hoy + 7 
(hoy..dentroDeSieteDias).each { dia -> 
    println dia 
} 
	

Otro ejemplo de clase soportada en rangos es String:

('a'..'z').each { letra -> 
    println letra 
} 
	

Los rangos ofrecen mucho más de lo aquí explicado, y como siempre te invito a que visites la documentación oficial de Groovy. Pero antes de terminar esta sección, veamos un último ejemplo que seguro te resulta útil en el futuro: rangos como valores de comparación en un bucle switch:

def sueldo = 1700; 
switch(sueldo) { 
    case 600..<1200: 
        println 'nivel 1' 
        break 
    case 1200..<1800: 
        println 'nivel 2' 
        break 
    case 1800..<2400: 
        println 'nivel 3' 
        break 
} 
	

El código es, sin duda, autoexplicativo.

3.2 Colecciones

Groovy ofrece soporte nativo para colecciones de tipo List y Map, así como conversión explícita de listas en colecciones de tipo Set y Queue. Veamos uno por uno estos tipos.

3.3 Listas

Las listas son colecciones secuenciales cuyos elementos pueden ser accedidos mediante un índice. Veamos como crear una lista vacía en Groovy:

def lista = [] 
println lista.class 
	

El código anterior define una lista sin elementos y la almacena en la variable lista. A continuación imprime su tipo (ArrayList en mi versión de Groovy, 1.7.2). Es posible insertar elementos en la lista en el momento de su creación:

def paises = ["España", "Mexico"] 
println paises.size() 
	

En el código anterior hemos definido una lista con dos elementos de tipo String. A continuación hemos llamado al método size() que nos devuelve el número de elementos de una lista. Ahora es el momento de añadir elementos a una lista previamente creada. Las dos maneras mas comunes de hacerlo son a través del operador leftshift (<<), un operador sobrecargado como se explicó en la sección 1.13, y del método add() del interface List, como habríamos hecho hasta ahora en Java:

paises << "Argentina"
paises.add("Ecuador")
println paises
	

Las dos primeras lineas del código anterior añaden un String cada a la lista paises. Ahora podemos obtener cualquier objeto de la lista por medio de su índice numérico, y también de dos maneras: mediante notación de arrays y mediante el método getAt():

def pais = paises[0]
def otroPais = paises.getAt(1)
	

Recuerda que las posiciones de una lista están basadas en índice cero: el primer elemento está en la posición 0, el segundo elemento en la posición 1, etc. La notación de arrays nos permite, además de sobreescribir el valor de una posición, insertar un nuevo elemento en una posición explícita, añadiendo valores null en todas las posiciones intermedias:

paises[3] = "Colombia"
paises[6] = "Ecuador"
	

El código anterior sobreescribe la posición número 3 con el valor Colombia, y a continuación define en la posición 6 el valor Ecuador. Las posiciones 4 y 5, las cuales aún no han podido ser definidas pero deben existir para mantener la secuencialidad, son 'rellenadas' con valores null; así mantenemos intacto el índice de la lista. Tras ejecutar el código anterior: nuestra lista de países quedara de la siguiente manera:

[España, Mexico, Argentina, Colombia, null, null, Ecuador]
	

Otra opción necesaria en una colección es la de eliminar objetos, operación que podemos llevar a cabo mediante el método remove(), el cual admite como argumento tanto el índice de la posición como el objeto a eliminar, y devuelve el valor eliminado:

def eliminado1 = paises.remove(6)
def eliminado2 = paises.remove("Ecuador")
	

Ambas líneas del código anterior realizar la misma operación para los valores concretos de nuestra lista: eliminar la posición 6 / eliminar el valor "Ecuador". Es importante que tengas en cuenta que si eliminamos por índice, y este no existe, obtendremos una excepción de tipo NullPointerException. Si eliminamos pasando un objeto que no existe obtendremos como resultado null, pero sin que se lance ninguna excepción. Antes de continuar vamos a eliminar los dos valores null que están embebidos en nuestra lista:

2.times { 
    paises.remove(null) 
} 
	

El código anterior ejecuta el método times de la clase Number (y por tanto heredado por Integer). Este método ejecuta el closure pasado como argumento tantas veces como el valor de la variable donde es invocado, en nuestro caso 2.

Una tercera manera de eliminar un elemento de una lista es mediante el método pop(), el cual devuelve y a elimina el último elemento de la lista (no el último elemento añadido, que podría haber sido insertado a través de notación de arrays en medio de la lista, si no el elemento que tiene el índice más alto). Este método permite tratar una lista como una cola LIFO (Last In First Out, el último en entrar el primero en salir) aunque, como veremos después, existe una manera más adecuada de crear colas de datos:

def eliminado = paises.pop()
println eliminado
println paises
	

Ahora el contenido de nuestra lista será el siguiente:

[España, Mexico, Argentina, Colombia]
	

Veamos ahora como iterar a través de los elementos de una lista:

paises.each { 
    println it.toUpperCase() 
} 
	

En el código anterior hemos llamado al método each(), el cual acepta un closure que es ejecutado por cada elemento de la lista. Dentro del closure hemos imprimido en mayúsculas cada elemento (recuerda que está lista contiene elementos de tipo String). Como explicamos en la sección 2.5, podemos utilizar una variable definida por nosotros cuando el closure acepta un único parámetro, sustituyendo así a la variable explicita it y haciendo el código más expresivo:

paises.each { pais -> 
    println pais.toUpperCase() 
} 
	

Una segunda manera de iterar una lista es mediante el método eachWithIndex, el cual acepta un closure con dos parámetros: uno para el valor de cada elemento iterado y otro para almacenar su posición (o número de iteración):

paises.eachWithIndex { pais, indice -> 
    println "${pais} se encuentra en la posición ${indice}" 
} 
	

Existe una forma alternativa de iteración, que ejecuta una acción en cada elemento de la lista y devuelve una nueva lista conteniendo el resultado de dichas ejecuciones. Esto es llevado a cabo mediante el método collect():

def paisesMayusculas = paises.collect { pais -> 
    pais.toUpperCase() 
} 
	

En el código anterior hemos creado una nueva lista, llamada paisesMayusculas que contiene el resultado de convertir en mayúsculas todos los valores de la lista países:

[ESPAÑA, MEXICO, ARGENTINA, COLOMBIA]
	

En determinadas ocasiones necesitamos ordenar una lista de manera natural, esto es, en base a los objetos que están contenidos en ella. Por ejemplo, para nuestra lista conteniendo objetos String la ordenación natural sería la alfabética:

paises.sort()
	

El fragmento de código anterior ordena la lista paises, modificando la lista original y dejando sus elementos (del tipo String) en orden alfabético):

[Argentina, Colombia, España, Mexico]
	

Otro método de utilidad es reverse() el cual devuelve una lista en sentido inverso, pero sin modificar la lista original. Por tanto, para capturar la nueva lista debemos asignarla a una variable:

def paisesInvertidos = paises.reverse()
	

Groovy nos permite añadir/sustraer una lista a/de otra mediante los operadores += y -=. Veamos un ejemplo:

def pares = [2, 4, 6, 8]
def impares = [1, 3, 5, 7, 9]
pares += impares
pares.sort()
pares -= impares
	

Las dos primeras lineas del código anterior crean dos listas, una conteniendo números pares y otra conteniendo números impares. A continuación, en la tercera linea, añadimos la lista de números impares a la lista de números pares, y en la cuarta linea ordenamos la nueva lista mixta con sort() (como la lista contiene números enteros, la ordenación se realiza por valor numérico, esto es: 1, 2, 3, 4, ...). En la quinta y última linea, eliminamos de la lista ordenada todos los valores de la lista de números impares, dejándola evidentemente solo con los valores pares originales.

Otro método de gran utilidad es join(), el cual une los valores de una lista en una cadena de texto:

def letras = ['a', 'b', 'c'] 
println letras.join() 
println letras.join("-") 
	

La primera llamada a join devuelve una cadena de texto conteniendo el valor abc, mientras que la segunda llamada devuelve también una cadena de texto pero, esta vez, con los valores de la lista separados por el String que hemos pasado como argumento; en nuestro caso, en el que hemos pasado como argumento un carácter de guión, el resultado es a-b-c. Ninguno de las versiones de join() modifica la lista original.

Groovy añade todavía más métodos de utilidad en las listas: max() y min, por ejemplo, devuelven los valores máximo y mínimo contenidos en una lista (como siempre, el concepto de máximo, mínimo, ordenación, etc. está relacionado con el tipo de objeto que almacena la lista):

println letras.max()
println letras.min()
	

En el código anterior, el método max() devuelve el valor c, ya que la ordenación de cadenas de texto en Java establece que c es un carácter 'mayor' que a o b. De igual manera, en la siguiente lista conteniendo números flotantes, el valor máximo correspondería a 4.71 y el valor mínimo a 0.02:

def flotantes = [2.08, 0.02, 4.71, 3.99, 1.99] 
println flotantes.max() 
println flotantes.min() 
	

Como puedes ver, el soporte de listas en Groovy es tremendamente potente a la vez que sencillo. Aquí solo se han explicado algunos métodos de los muchos que provee el API, así que te recomiendo que estudies la documentación para encontrar información adicional.

3.4 Array's, Set's y colas

Por defecto, todas las listas en Groovy son implementaciones de la clase ArrayList. Sin embargo, podemos coaccionar al lenguaje para que convierta una lista en una implementación de Set (en la versión 1.7.2 HashSet), de Queue (LinkedList) o en un array:

def setPaises = paises as Set 
def colaPaises = paises as Queue 
def arrayPaises = paises as String[] 
println setPaises.class 
println colaPaises.class 
println arrayPaises.class 
	

También podemos aplicar la sintaxis anterior en el momento de la creación de la colección, para así trabajar con el tipo deseado desde el primer momento:

def paises = ["España", "Mexico"] as Set
	

Y por supuesto, podemos seguir instanciando nuestras colecciones mediante la antigua sintaxis de Java. Recuerda que el 99% del código Java es código Groovy válido:

Set lhs = new LinkedHashSet();
	

3.5 Map's

Los mapas (maps) son colecciones de valores que pueden ser referidas por una clave, o lo que es lo mismo, una colección de parejas clave-valor. Groovy ofrece soporte nativo también para mapas, con una sintaxis que en gran medida es muy similar a la de las listas:

def mapa = [:]
	

El código anterior crea un mapa vacío, en el cual podemos, por supuesto, ir añadiendo parejas clave-valor (posteriormente veremos como). Si deseas inicializar un mapa con valores predefinidos:

def capitales = ['España':'Madrid', 'Mexico':'Mexico D.F.']
	

El código anterior define un mapa con 2 parejas clave-valor en su interior. Veamos ahora como realizar algunas operaciones básicas con mapas:

capitales.get('España')
capitales.España
capitales['España']
	

Las tres lineas de código anterior realizan la misma tarea, obtener un valor a partir de su clave. La primera utiliza el método get(clave) para obtener el valor asociado a dicha clave. La segunda línea utiliza la notación de puntos, en la forma mapa.clave. La última versión utiliza la notación de arrays. Cual uses depende en gran medida de tus preferencias. Es importante subrayar que cuando utilices notación de puntos debes encerrar entre comillas el nombre de las claves que contengan ciertos caracteres especiales, por ejemplo, un carácter de punto:

def datos = ['correo.electronico', 'programacion@davidmarco.es']
println datos.'correo.electronico'
	

El mapa definido en el código anterior contiene una clave que contiene un carácter de punto, de manera que al usar notación de puntos debemos encerrar el nombre de la clave entre comillas (tanto comillas simples como dobles están permitidas). Si no lo hacemos, obtendremos un error de compilación, ya que al haber más de un punto este no sabría donde comienza el nombre de la clave. Veamos ahora como añadir elementos a un mapa:

capitales.put('Argentina', 'Buenos Aires')
capitales.Argentina = 'Buenos Aires'
capitales['Argentina'] = 'Buenos Aires'
	

Las tres lineas de código anterior insertan una nueva pareja clave-valor en el mapa capitales. Al igual que al obtener un valor, cuando insertamos mediante notación de puntos un valor que contiene un punto mediante debemos encerrar la clave entre comillas. La última operación básica que nos queda es la de borrar una pareja clave-valor mediante el método remove(clave):

capitales.remove('Argentina')
	

La forma de iterar las parejas clave-valor de un mapa es tan sencilla como en una lista:

capitales.each { 
    println it 
} 
	

El código anterior mostraría las parejas como tales. Si deseamos acceder a las claves y valores de forma independiente tenemos dos opciones: la primera consiste en usar los atributos key y/o value de la variable que almacena la pareja en cada iteración:

capitales.each { 
    println it.key 
}
	

La segunda manera consiste en pasar como parámetro a each() un closure con dos variables, la primera de ellas haciendo referencia a la clave correspondiente y la segunda a su valor:

capitales.each { pais, capital -> 
    println "La capital de ${pais} es ${capital}" 
} 
	

Otros muchos métodos que vimos en la sección 3.3 (LISTAS) también están disponibles para mapas, como son collect() y sort(). Otros tienen una sintaxis ligeramente diferente (como reverseEach(Closure) en lugar de reverse()). Visita el API de Groovy para ver todos los métodos disponibles en Map. Otras operaciones, como adición y sustracción entre mapas están, también soportadas, aunque de nuevo de una manera un tanto diferente a como las realizábamos con listas. Por ejemplo, la adición de una lista a otra funciona de manera igual, mediante el operador +=:

def angloParlantes = ['EEUU':'Washington', 'Reino Unido':'Londres']
capitales += angloParlantes
	

Sin embargo, la sustracción mediante el operador -= no está soportada en mapas, por lo que hay que realizar un poco de trabajo extra para llevar a cabo esta operación:

angloParlantes.each { 
    capitales.remove(it.key) 
} 
	

En el código anterior iteramos a través del mapa que queremos eliminar, seleccionamos cada uno de las claves, y se las pasamos al método remove del mapa del cual queremos sustraer los valores.

Antes de terminar esta sección vamos a ver cuatro nuevos métodos. Los dos primeros métodos son keySet() y values(). El primero devuelve una lista con todas las claves de un mapa, mientras que el segundo devuelve una lista conteniendo todos los valores de un mapa:

def paises = capitales.keySet() 
	

El código anterior almacenaría en la variable paises una lista con las claves del mapa capitales:

[España, Mexico, Argentina] 
	

Por ultimo, los métodos containsKey() y containsValue() devuelven true o false dependiendo de si el mapa donde los llamemos contiene cierta clave o cierto valor:

println capitales.containsKey('España')
println capitales.containsValue('Roma')
	

Para el código anterior, la primera linea devolverá true mientras que la segunda devolverá false.

Resumen

En este capítulo hemos visto diversos tipos de colecciones, como rangos, listas y mapas. El API de estos objetos es mucho mas amplio de lo que, por motivos de espacio, podemos (y tal vez debemos) mostrar aquí. Sin embargo, con lo aquí citado podemos comenzar a trabajar cómodamente con todos estos tipos de colecciones. En posteriores artículos las veremos y usaremos de estas y otras formas. En el próximo artículo veremos como trabajar con POGO's, el equivalente Grooviano del clásico POJO. Hasta entonces, ¡un saludo!.