Generics (I) – Introducción

Java Generics es una de las características más solicitadas de Java (casi desde la primeras versiones). La implementación vio la luz en el año 2004 (offtopic: año en que desgraciadamente falleció el gran Miguel de Guzmán).

Previo

Me gustaría hacer un inciso sobre los posibles errores que pueden existir en una aplicación. Voy a tomarme la libertad de clasificarlos en tres conjuntos que, aunque totalmente inventados, creo que pueden ser considerados perfectamente válidos:

  1. Errores en tiempo de compilación. Los mejores errores que podemos tener, ya que los detectamos en el momento que compilamos la aplicación. Estos errores son los más fáciles de detectar.
  2. Errores en tiempo de ejecución. Errores que pasan desapercibidos para el compilador. Sólo aparecen mientras el sistema está en funcionamiento, ya sea durante su testeo (todo tipo de pruebas) como durante su explotación (cuando el cliente lo está usando).
    1. Errores en tiempo de ejecución chungos. Dentro de los errores en tiempo de ejecución, me inventaré la categoría «chungos», aquellos errores que por unas causas o por otras los detecta el usuario final en lugar de los desarrolladores (o el equipo de pruebas). Vienen a ser la maldad convertida en bug.

Pero … ¿por qué usar generics?

Supongamos que en algún momento a lo largo de nuestra vida hemos llegado a tener el engendro de clases que se muestra en la figura.

Jerarquía de alimentos

Vemos que tanto verdura como carne son comida y pueden ser cocinadas dando lugar a comida cocinada (menos mal). Por otro lado, vamos a suponer que tenemos una clase que se corresponde con un cocinero de comida vegetariana. Éste tomará una colección de verduras y las devolverá bien hechas para que se las coma el usuario:

class VeganChef {
  // ...
  public Collection cook(Collection vegetables) {
    List cookedFood = new ArrayList();
    for(Object obj : vegetables) {
      Vegetable vegetable = (Vegetable) obj;
      cookedFood.add(vegetable.getCooked());
    }
    return cookedFood;
  }
  // ...
}

(Me he tomado la libertad de usar un bucle de tipo for-each, porque me resultan más cómodos que iteradores a saco. De todas formas, el bucle de la forma for-each no se introdujo hasta la versión 1.5.)

Como podemos ver, el código fuente no parece tener ningún error. El problema es que, a priori el código es completamente correcto y compilará sin problemas. De hecho, normalmente no debería dar problemas en ejecución. Pero ¿qué sucedería si en lugar de…

//...
Set vegetables = new HashSet();
vegetables.add(new Bean());
vegetables.add(new Carrot());
Collection cookedFood = veganChef.cook(vegetables);
//...

… hubiésemos escrito lo siguiente?

// ...
Set integers = new HashSet();
integers.add(new Integer(1));
integers.add(new Integer(2));
// ...
Collection cookedFood = veganChef.cook(integers);
// ...

Lo que ocurriría es un gran error en el punto que hacemos el casting, ya que un entero no es una verdura (de momento):

Vegetable vegetable = (Vegetable) obj; // ERROR!

El problema de este error es que es indetectable en tiempo de compilación. En nuestro caso fallaría en el momento de hacer el casting y quizá sería fácilmente detectable en algún punto durante la fase de pruebas. El verdadero problema es, cuando seis semanas después tenemos que nuestra jerarquía de clases ha sido modificada:

Jerarquía de alimentos

y alguien «sin querer» ha escrito lo siguiente:

// ...
Set food = new HashSet();
food.add(Ternera);
food.add(Cordero);
// ...
Collection cookedFood = veganChef.cook(filets);
// ...

Esto es un problema. El error es prácticamente indetectable hasta que el usuario final (en nuestro caso, el pobre comensal vegetariano) da la voz de alarma. Y que el usuario de un producto se sienta beta-tester cuando no lo es no deja muy buena impresión. A estos errores me refería cuando al principio del post decía «chungos».

Solución

Usando generics, nuestro método de cocinar podría quedar como sigue:

class VeganChef {
// ...
public Collection cook(Collection<Vegetable> vegetables) {
List cookedFood<Vegetable> = new ArrayList<Vegetable>();
for(Vegetable vegetable : vegetables) {
cookedFood.add(vegetable.getCooked());
}
return cookedFood;
}
// ...
}

Estamos indicando que lo que recibimos es una colección de verduras, de tal forma que en los dos ejemplos anteriores nos daría un error de compilación:

Set filets = new HashSet();
filets.add(Ternera);
filets.add(Cordero);
// ...
Collection cookedFood = veganChef.cook(filets); // ERROR
// ...

Esto es una gran ventaja. Hemos bajado un error del nivel de error de ejecución chungo (nos falla en ejecución, y probablemente en un caso que no habíamos contemplado y que se ha «comido» el usuario final) en un error en el nivel de compilación (los más fáciles de arreglar). Hemos conseguido hacer la programación genérica en Java menos propensa a errores.

Para finalizar (de momento)

No obstante, el uso de Generics no está exento de sus peculiaridades. Si las usas, habrás oído decir, con mejores o peores palabras, que las Generics en Java no tienen nada que ver con las Templates de C++. Bueno, en realidad ambas son mecanismos para facilitar el concepto de programación genérica en ambos lenguajes, pero sí es cierto que la forma de comportarse de ambos es bastante diferente.

En los próximos artículos se resaltarán estos aspectos, además de cómo usar clases que se declaran como genéricas e incluso cómo declararlas nosotros como solución a determinados problemas. Espero que os haya gustado. Tanto si sí, como si no, ¡ahí tenéis los comentarios!

PD: Los diagramas han sido generados a partir de la aplicación http://yuml.me/. Es Web, gratuita (aunque existe versión de pago) y permite generar fácil y rápidamente diagramas de secuencia, de clases y de casos de uso.

1 opinión en “Generics (I) – Introducción”

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *