Artículos propios
NovedadesBúsqueda avanzadaNueva referenciaTop 10

Implementación de Caches Inteligentes Mediante "Soft References"

Cómo podemos mejorar el rendimiento y la robustez de nuestras aplicaciones utilizando "Soft References", para evitar problemas de escasez de memoria durante picos de carga imprevistos.

Fecha de creación: 02.10.2001
Revisión 1.0 (25.04.2005)

Daniel López Janáriz
D DOT Lopez AT uib DOT es

Copyright (c) 2003, Daniel López Janáriz. Este documento puede ser distribuido solo bajo los términos y condiciones de la licencia de Documentación de javaHispano v1.0 o posterior (la última versión se encuentra en http://www.javahispano.org/licencias/).

Introducción: Utilización de caches

En casi todos los sistemas informáticos podemos encontrar operaciones que se repiten frecuentemente, a veces con ligeras variaciones y a veces exactamente igual una y otra vez. Por ello, una técnica muy habitual para aumentar el rendimiento de estos sistemas es almacenar, para su posterior reutilización, los resultados parciales de las operaciones que más se repiten. Dicha técnica, que suele denominarse vulgarmente como "implementación de una cache", en alusión a la memoria intermedia donde se almacenan los resultados parciales, nos evita tener que calcular cada vez las operaciones completas, con el consiguiente ahorro en tiempo que ello supone.

Figura 1: Funcionamiento básico de un mecanismo de cache

La implementación de "caches", muy utilizada en sistemas hardware, puede sernos también muy útil en nuestras aplicaciones en Java. Un ejemplo muy típico de ello se da en las aplicaciones web que utilizan XML y XSLT para generar la interfaz HTML. Aunque la explicación de cómo se utiliza XML y XSLT para generar HTML queda fuera del ámbito de este artículo, baste decir que, usar una "cache" para almacenar las hojas XSLT una vez han sido compiladas, puede significar una mejora del rendimiento de hasta un 200%.

A continuación, mostraremos como podemos organizar nuestros programas en Java para introducir una "cache", después veremos como podemos mejorar el diseño.

Para implementar una "cache", lo primero que haríamos sería evitar que el programa principal llame directamente al método que calcula los resultados intermedios. Así pues, dejaremos un hueco para poder introducir la "cache". Mostramos como hacerlo en el listado 2.

...
/**
 * Método intermedio utilizado por el programa principal para 
 * obtener un calculo intermedio.
 */
public Object getResultadoIntermedio(Object parametros)
{
 // Llama directamente al método para calcular los resultados.
 // Luego añadiremos aquí la "cache".
 return calcularResultadoIntermedio(parametros)
}

/**
 * Método para calcular resultado intermedio.
 * Dicho método no es llamado directamente si no que 
 * únicamente se le puede llamar a través del método
 * "getResultadoIntermedio".
 */
 Object calcularResultadoIntermedio(Object parametros)
 {
  ...//implementación de los cálculos
 }
 ...
Listado 2

En el listado 3 modificamos el método getResultadoIntermediopara incluir la "cache". El punto importante reside en el hecho de que ahora, si el resultado intermedio ya está almacenado en la "cache", no tenemos que volver a calcularlo si no que lo devolvemos directamente.

/**
 * Hashtable que representara nuestra "cache"
 */
 Hashtable laCache;
 
 /**
 * Método utilizado para recuperar un objeto de la
 * cache. Si está ya almacenado, sólo lo devuelve, si no
 * lo crea primero, lo almacena en la cache para usos posteriores
 * y después lo devuelve.
 */
 public Object getResultadoIntermedio(Object parametros)
 {
  Object resultadoTemporal = null;
  // Primero comprobaremos si la cache ha sido
  // inicializada. Si no es así, lo hacemos ahora.
  // Nota: Ponemos la comparación al revés para evitar la
  // equivocación de poner una asignación (this.laCache = null)
  if(null == this.laCache) 
  {
   this.laCache = new Hashtable();
  }
  // Comprobamos si ya está en la cache
  if(this.laCache.containsKey(parametros))
  {
   // Si está, simplemente lo devolvemos
   resultadoTemporal = this.laCache.get(parametros);
  }
  else
  {
   // Si no está, lo calculamos
   resultadoTemporal = calcularResultadoIntermedio(parametros);
   // Y después lo almacenamos para posterior reutilización
   this.laCache.put(parametros,resultadoTemporal);
  }
  return resultadoTemporal;
 }
Listado 3

La implementación de "caches" puede incrementar significativamente el rendimiento de nuestras aplicaciones.

Problema principal: El crecimiento desmesurado de la cache

Una vez ya sabemos como implementar una "cache" en nuestros programas, debemos tener en cuenta los problemas que nos puede ocasionar. Para ello tendremos en cuenta que la utilidad real de dicha técnica depende en gran medida de dos factores fundamentales:

  • El tamaño de la "cache" donde almacenaremos los resultados parciales
  • Los mecanismos de gestión de la "cache"

Un tamaño inadecuado o una mala gestión de la "cache" pueden transformar en inútiles nuestros esfuerzos para mejorar el rendimiento de nuestra aplicación.

La mayor dificultad a la que nos enfrentamos al implementar una "cache" en nuestros programas en Java es cómo controlar el consumo de memoria. Por un lado, si imponemos un limite artificial al tamaño de la "cache" corremos el riesgo de que sea insuficiente, y no consigamos sacarle el máximo rendimiento al sistema. Por otro lado, si no imponemos un límite, nos arriesgamos a que la "cache" consuma toda la memoria y a que nuestro programa aborte con una "OutOfMemoryException". La solución sería implementar una "cache" que se pudiera adaptar inteligentemente a la memoria disponible, y que liberase parte de su contenido cuando la aplicación lo necesitara. Suena bien. ¿Verdad? Pues una implementación de este tipo, la cual sería bastante complicado de realizar de forma manual, se puede conseguir fácilmente gracias a uno de los últimos avances introducidos en el API de Java: Las "Soft References"

El mayor problema de las "caches" es el consumo de memoria, puesto que su crecimiento desmesurado puede consumir toda la memoria disponible

Las "Soft References"

Las "Soft References" introducidas en la versión 1.2 del API de Java, en el paquete java.ref, son referencias especiales que se usan para almacenar objetos que pueden ser reciclados por el Garbage Collector(GC) cuando la Máquina Virtual Java(JVM) necesite más memoria. Es decir, en caso de que nuestro programa requiera memoria adicional, el GC se activa y recicla todos los objetos que ya no se utilizan para poder hacer uso ese espacio de memoria. Hasta aquí todo normal. Sin embargo, si una vez reciclado ese espacio aun necesitamos más memoria, el GC tiene permiso para reciclar aquellos objetos que sean únicamente accesibles por "Soft References".

Figura 2: Las "Soft References" permiten al GC reciclar los objetos a los que apuntan

¿Cómo podemos utilizar las "Soft References" para mejorar nuestras "caches" en Java? Muy fácil: En vez de almacenar en la "cache" referencias directas a los resultados intermedios, almacenaremos dichos objetos pero apuntados únicamente a través de "Soft References". Mostramos cómo hacerlo en el listado número 4.

...
/**
 * Método utilizado para recuperar un objeto de la
 * cache. Si está ya almacenado, sólo lo devuelve, si no
 * lo crea primero, lo almacena en la cache para usos posteriores
 * y después lo devuelve. El almacenamiento en la cache
 * se hace a través de Soft References
 */
public Object getResultadoIntermedio(Object parametros)
{
 ... // Hasta aquí todo igual.
 // Comprobamos si ya está en la cache
 if(this.laCache.containsKey(parametros))
 {
  // Si está, lo intentamos recuperar 
  SoftReference laReferencia = (SoftReference)this.laCache.get(parametros);
  // Si el resultado ha sido reclamado por el GC
  // la SoftReference devuelve null y tendremos que
  // calcular de nuevo el resultado
  resultadoTemporal = laReferencia.get();
 }
 // Si no estaba en la cache o ha sido reclamado por el GC,
 // lo calculamos
 if(resultadoTemporal==null)
 {
  resultadoTemporal = calcularResultadoIntermedio(parametros);
  // Y después lo almacenamos para posterior reutilización
  // para almacenarlo usaremos una SoftReference y así
  // permitimos al GC reclamar al objeto en caso necesario
  SoftReference referenciaIntermedia = new
  SoftReference(resultadoTemporal);
  this.laCache.put(parametros, referenciaIntermedia);
 }
 return resultadoTemporal;
}
Listado 4

Figura 3: Implementación de una cache con Soft References

Estos cambios tan simples permite al GC reciclar en caso necesario los objetos almacenados en nuestra "cache", de forma que no nos quedemos sin memoria en nuestro programa por tener la "cache" demasiado llena. En el programa principal no tenemos que cambiar nada ya que hemos encapsulado la utilización de la "cache" dentro de un método que se encarga de todo.

A través de las "Soft References" damos permiso al GC para que pueda reclamar algunos objetos en caso de emergencia, aunque sigan teniendo referencias desde nuestro programa.

A la hora de recuperar los objetos de la "cache" para su posterior reutilización hay que tener en cuenta algunos detalles:

  • Como el GC podría ponerse en marcha en cualquier momento y eliminar el objeto que queremos usar, conviene comprobar siempre los objetos antes de usarlos para confirmar que no han sido reciclados.
  • Para asegurarnos que el GC no eliminará el objeto mientras estamos trabajando con él, basta con apuntarlo con una referencia normal ya que el GC sólo puede reclamar aquellos objetos que sólo estén apuntados por "Soft References". Por ello es conveniente asignarle al objeto una referencia normal, en vez de trabajar directamente con la "Soft Reference" y utilizar SoftReference.get().
  • Con la misma idea que el punto anterior, para que nuestra técnica no pierda su utilidad hay que tener cuidado de no guardar ninguna referencia directa a los objetos almacenados en la "cache". Por eso el programa principal debe desechar las referencias a los resultados intermedios en cuanto le sea posible.
Figura 4: Usando una cache con Soft References

Analizando los cambios, podemos ver que añadiendo unas simples líneas de código mejoramos el comportamiento de nuestro programa haciéndolo más robusto. Esta técnica es de especial utilidad en aplicaciones que pueden recibir aleatoriamente picos de carga muy elevados, como es el caso de muchas aplicaciones web. Durante estos periodos es admisible que el rendimiento de la aplicación baje, pero de ningún modo podemos permitir que la aplicación se bloquee. Gracias a esta técnica aseguramos que, en caso de emergencia, el espacio utilizado por la "cache" pueda ser reclamado, y que, aunque el rendimiento baje, la aplicación sobreviva. Cuando el pico de carga termine, la aplicación podrá usar el espacio sobrante para la "cache" y aumentar de esa forma el rendimiento.
Cabe resaltar que la implementación mostrada en los listados anteriores es muy simple y que, por ejemplo, no contempla el acceso concurrente a la "cache". En un apartado posterior ofreceremos una versión más depurada de cómo podríamos implementar la misma "cache" teniendo en cuenta estos problemas y utilizando un diseño orientado a objetos.

Unas pocas lineas de código pueden mejorar significativamente la robustez de nuestro programa, protegiéndolo frente a picos de carga.

Problemas a evitar

En este apartado describiremos cuáles son los principales riesgos a los que nos enfrentaremos al usar "Soft References" y qué debemos hacer para evitarlos.
La primera dificultad asociada al uso de esta técnica, y que debemos evitar, es que, al dejar completamente al cargo del GC la gestión de la memoria, podemos estar encubriendo un fallo en el cálculo de la memoria que necesita nuestro programa para ejecutarse normalmente. Es decir, si la memoria asignada en condiciones normales a nuestro programa es insuficiente, gracias al uso de "Soft References" evitaremos que el programa aborte con un error por falta de memoria, pero el GC se ejecutará muy a menudo y nuestra "cache" estará continuamente llenándose y vaciándose. Esta circunstancia produciría un deterioro bastante importante del rendimiento de nuestro programa. Por esta razón conviene controlar de alguna forma, por ejemplo a través de ficheros de trazas(vulgarmente conocidos como logs) el uso que se está haciendo de la "cache", si el GC la vacía muy a menudo etc.
Si el GC reclama muy a menudo los objetos de la "cache" entonces debemos revisar la memoria asignada al programa y comprobar que es la adecuada para un funcionamiento normal. La técnica de las "Soft References" debe ser utilizada para dar robustez a los programas frente a picos anormales de carga, pero no debe usarse para evitar tener que controlar la memoria utilizada.

Un segundo problema, muy similar al primero, se daría cuando el crecimiento de la "cache" es demasiado grande debido a que el numero de objetos a almacenar es "ilimitado". Entendiendo en este caso por ilimitado un número lo suficientemente grande para que no nos quepa en la memoria de la que disponemos. Este caso es frecuente cuando se almacenan objetos de una base de datos con muchos elementos, o cuando los objetos son muy grandes (ficheros de sonido, imágenes...). De nuevo nos encontramos con el mismo problema que en el punto anterior, ya que el GC se ejecutaría continuamente deteriorando el rendimiento. Sin embargo, esta vez no podemos solucionarlo configurando adecuadamente la memoria asignada a nuestro programa puesto que ya sabemos que será insuficiente. Aparte de la opción obvia de no hacer nada y dejar que el GC haga lo que pueda, tenemos varias opciones cuyo denominador común es añadirle más inteligencia a la "cache" para evitar que el GC tenga que ejecutarse tan a menudo.

  • Una primera opción, si podemos determinar a priori cuales son los elementos que más se utilizan, es limitar de alguna forma el tamaño de la "cache" y almacenar en ella sólo dichos elementos. El resto de elementos tendrían que ser calculados de nuevo cada vez y con ellos perderíamos para ellos la ventaja de la "cache", pero si calculamos bien cuales son los elementos más utilizados podemos conseguir un buen equilibrio entre utilización de memoria y rendimiento.
  • La segunda opción sería algo más compleja y se basaría en almacenar en la cache los objetos que más se utilizan a través de referencias normales y el resto usando "Soft References". De esta forma nos aseguraríamos que el GC no reclama nunca los objetos que más se utilizan y le dejaríamos reclamar el resto en caso necesario. La clasificación de objetos según se usen más o menos podría hacerse dinámicamente aunque ello aumentaría la complejidad de nuestra "cache".

Por último, sólo reseñar que si en nuestra aplicación el numero de objetos utilizados es muy alto y no se puede determinar un grupo de objetos que se utilice más que el resto, entonces no tiene sentido usar una "cache", por lo que la técnica de las "Soft References" tampoco.

Debemos tener cuidado al usar la técnica de las "Soft References" para que la ejecución continuada del GC no perjudique el rendimiento de nuestro programa.

Notas sobre la implementación

Ahora pasaremos a tratar algunos detalles de la implementación mostrada en el listado 4 y ofreceremos una implementación alternativa que se ajusta más a un buen diseño orientado a objetos.

Problemas del listado 4

Como ya hemos mencionado, la implementación del listado 4 es muy simple y contiene algunos problemas o defectos que podríamos mejorar.
Entre ellos:

  • El acceso a la "cache" no está sincronizado y esto podría causarnos algún quebradero de cabeza si la accedemos desde un servlet, por ejemplo, que permita un acceso concurrente. La solución está bastante clara y no es otra que sincronizar el acceso a la "cache" y sus contenidos.
  • Un problema que debemos tener en cuenta al usar un objeto Hashtable como "cache" es la clase objetos que vamos a usar como claves. Explicar en profundidad dicho problema y cómo solucionarlo no es tema de este artículo, pero baste mencionar que si se usa como clave una clase propia, en vez de una clase básica del API como String o Integer, hay que acordarse de implementar adecuadamente los métodos equals y hashcode en esa clase. Para más información, consultar la documentación del API sobre la clase java.util.Hashtable.
  • Por último, como problema de diseño podríamos mencionar que la implementación que hemos mostrado, encapsulando la "cache" en un método, no es lo más adecuado desde el punto de vista de la orientación a objetos. Por eso es mejor crear una nueva clase que encapsule completamente la "cache" y sus métodos de acceso. Este sistema nos permitiría además sincronizar el acceso de una forma más sencilla.

Nueva implementación

Para solucionar dos de los problemas mencionados en el apartado anterior crearemos una nueva clase que encapsule la "cache" y sincronice el acceso para que no tengamos problemas al usarla concurrentemente.

A la hora de crear una nueva clase se nos presenta una típica disyuntiva del diseño orientado a objetos: ¿herencia o composición? Dado que una Hashtable se adapta casi perfectamente a la idea que tenemos de una "cache", una primera idea sería heredar de la clase Hashtable y modificar los métodos put y get para que usaran "Soft References". Sin embargo, dado que la clase Hashtable implementa más métodos para manejar los datos, como putAll, remove... entonces también tendríamos que implementarlos para evitar que se usaran de forma incorrecta. Además, si más adelante quisiéramos añadirle inteligencia a nuestra "cache", diferenciando, por ejemplo, entre los objetos más utilizados y el resto, entonces quizá nos convendría usar más de una Hashtable.
Por ello nos decantaremos por usar la técnica de la composición y crear una clase que contiene un objeto Hashtable como miembro. Además, para tener una implementación de "cache" independiente de nuestro programa, los objetos a reutilizar no los calculará directamente la "cache" si no que para ello utilizaremos una interfaz, que deberá ser implementada cualquier programa en el que queramos usar una "cache". Mostramos el resultado en los siguientes listados:

En el listado 5 se muestra el interfaz que deberá implementar cualquier programa que quiera utilizar una "cache", de esta forma evitamos tener que escribir el código que calcula los resultados intermedios dentro de la "cache", y así podemos utilizarla para cualquier tipo de programa.

package mis.utils;
/**
 * Interfaz que deben implementar las clases que quieren utilizar
 * una Cache. Esta interfaz será utilizada desde la Cache para 
 * poder crear los resultados intermedios, en caso de no tenerlos
 * ya almacenados
 */ 
public interface CalculadorResultados 
{
  /*
  * Cálcula un resultado intermedio en función de los parámetros.  
  */
  public Object calcularResultadoIntermedio(Object parametros);
}
Listado 5

En el listado 6 mostramos el código de la implementación de la "cache" propiamente dicha. Cabe resaltar el constructor, a través del cual le pasamos una referencia al objeto que implementa la interfaz del listado 5.

package mis.utils;
/**
 * Clase que encapsula una cache. Su implementación puede ir desde 
 * usar una simple Hashtable para almacenar los resultados a usar 
 * varias Hashtables y Soft References para implementar una cache
 * inteligente
 */
import java.util.Hashtable;

public class CacheSimple
{
 /**
  * Hashtable donde almacenaremos los objetos a reutilizar
  */
 private Hashtable tablaDeElementos;

 /**
  * Referencia al objeto que calculará los resultados a almacenar
  * en la cache en caso de que no lo tengamos ya almacenado.
  */
 private CalculadorResultados elCalculador;

 /**
  * Constructor, se encarga de inicializar todos los recursos
  */
 public CacheSimple(CalculadorResultados calculador)
 {
 this.tablaDeElementos = new Hashtable();
 this.elCalculador = calculador;
 }

 /**
  * Método para recuperar un objeto de la cache. Si no lo tenemos          
  * almacenado, lo calculamos a través de la referencia que 
  * tenemos al objeto calculador de resultados intermedios. El 
  * método está sincronizado para evitar problemas de acceso
  * concurrente.
  */
 public synchronized Object getResultado(Object parametros)
 {
  Object resultadoTemporal = null;
  // Comprobamos si ya está en la cache
  if(this.tablaDeElementos.containsKey(parametros))
  {
   // Si está, lo intentamos recuperar 
   SoftReference laReferencia = 
   (SoftReference)this.tablaDeElementos.get(parametros);
   // Si el resultado ha sido reclamado por el GC
   // la SoftReference devuelve null y tendremos que
   // calcular de nuevo el resultado
   resultadoTemporal = laReferencia.get();
  }
  // Si no estaba en la cache o ha sido reclamado por el GC,
  // lo calculamos
  if(resultadoTemporal==null)
  {
   resultadoTemporal = this.calculadorResultados.calcularResultadoIntermedio(parametros);
   // Y después lo almacenamos para posterior reutilización
   // para almacenarlo usaremos una SoftRefernce y así
  // permitimos al GC reclamar al objeto en caso necesario
   SoftReference referenciaIntermedia = new SoftReference(resultadoTemporal);
   this. tablaDeElementos.put(parametros, referenciaIntermedia);
  }
  return resultadoTemporal;
 }
}
Listado 6

Por último, en el listado 7 mostramos el esqueleto de un programa que utiliza una "cache".

/**
 * Clase que utiliza una "cache" para hacer su trabajo. Implementa
 * la interfaz CalculadorResultados para calcular los resultados
 * intermedios que se almacenaran en la ella.
 */
 import mis.utils.CalculadorResultados;
 import mis.utils.CacheSimple;
 
 public class ProgramaQueUsaCache
       implements CalculadorResultados
{
/**
 * La "cache".
 */
CacheSimple laCache;

/**
 * Implementación de la interfaz CalculadorResultados
 * Cálcula un resultado intermedio en función de los parámetros.        
 */
public Object calcularResultadoIntermedio(Object parametros)
{
 // Implementación del cálculo un resultado en función        de
 // los parámetros y devolución del objeto resultado
}

/**
 * Constructor, en el podemos inicializar la "cache", pasándole
 * una referencia al objeto mismo.
 */
public ProgramaQueUsaCache()
{
 laCache = new CacheSimple(this);
}
...
// A la hora de ejecutar nuestro programa, solo tenemos que preocuparnos de
// pedirle a la "cache" unos resultados
Objeto elResultado = laCache.getResultado(parametros)
...
}
Listado 7

Nota: Si quisiéramos perfeccionar aun más la implementación, podríamos usar una interfaz que se llamara mis.utils.Cache e implementar dicha interfaz de varias formas: Cache simple, Cache con "Soft References", Cache inteligente... Después escribiríamos nuestro programa principal de forma que utilizará la "cache" a través únicamente de la interfaz y podríamos incluso decidir en tiempo de ejecución qué implementación utilizar. Ello nos permitiría, entre otras cosas, independizar nuestro programa de la implementación de la "cache" y poder comprobar fácilmente cuales son los beneficios reales de usar una u otra implementación.

Utilizando apropiadamente la orientación a objetos, podemos hacer que nuestros programas sean más flexibles e independientes unos de otros.

Conclusión

Después de analizar las técnicas propuestas, sus ventajas y posibles inconvenientes, podemos sacar unas conclusiones para su correcta utilización:

  • Antes de implementar una "cache" en nuestros programas, debemos comprobar que se dan las circunstancias adecuadas para poder utilizarla correctamente. Es decir, que en nuestro programa tenemos una serie de elementos que cuesta mucho obtener, que se utilizan muchas veces, y que su número es limitado o que al menos el número de elementos que se utiliza más a menudo sí lo es. Dichos elementos pueden ser datos extraídos de un fichero o una base de datos, ficheros de imágenes, cálculos de algún tipo etc., lo importante es que si no se dan las circunstancias que hemos descrito, posiblemente no obtengamos ningún beneficio de implementar una "cache".
  • Una vez determinado que una "cache" nos es útil, podemos aumentar la robustez del programa añadiendo simplemente una lineas de código para que los objetos de la "cache" se almacenen a través de "Soft References". De esta forma, conseguimos que la memoria usada en nuestra "cache" pueda ser recuperada por nuestro programa en caso de emergencia.
  • Para no ocultar defectos de asignación de memoria a nuestro programa, es conveniente controlar el comportamiento de la "cache" y comprobar que el GC no esta continuamente ejecutándose, lo cual degradaría el rendimiento de nuestro programa. En caso de darse esta circunstancia, debemos revisar la memoria asignada a nuestro programa para ejecutarse, o introducir una "cache" más sofisticada que no consuma tanta memoria.
  • Es una buena práctica de programación introducir la "cache" de la forma más transparente posible. Con transparente queremos decir que, el hecho de estar usando una "cache", debe modificar lo menos posible la estructura de nuestro programa. De esta forma podremos comprobar más fácilmente si estamos obteniendo una mejora en el rendimiento ya que eliminar o deshabilitar la "cache" es mucho más sencillo.

Por último, resaltar que implementar un diseño orientado a objetos de la forma correcta nos ayudará a hacer nuestros programas más fáciles de depurar, mantener y reutilizar. Lo cual no es una mala idea ;).

Recursos

[1] Java http://java.sun.com/

[2] Reference API, http://java.sun.com/j2se/1.4.2/docs/guide/refobs/

[3] Artículo: Reference Objects and Garbage Collection, http://java.sun.com/developer/technicalArticles/ALT/RefObj/

[4] Ejemplo de utilización: Sección "Performance improvements" (p.30) de, http://java.sun.com/j2ee/white/eMobilePartII.pdf

Acerca del autor

Daniel López Janáriz
Daniel trabaja en el Centro de Tecnologías de la Información de la Universidad de las Islas Baleares (CTI@UIB.ES) como coordinador del desarrollo web. Además de programar en Java, diseñar aplicaciones web y administrar el Rincón Java, también le gusta invertir el tiempo con otras cosas importantes como salir con la novia, los amigos, ir al cine, leer... No todo es informática en la vida.